Start reorganising for multiple games
This commit is contained in:
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,14 +1,8 @@
|
|||||||
/config.toml
|
/config.toml
|
||||||
/loader
|
/loader
|
||||||
/orig
|
/isos
|
||||||
/palette-idx
|
/CG
|
||||||
/view-ani
|
/SL
|
||||||
/view-font
|
/SaW
|
||||||
/view-obj
|
/WoW
|
||||||
/view-map
|
/bin
|
||||||
/view-minimap
|
|
||||||
/view-menu
|
|
||||||
/view-set
|
|
||||||
/ordoor
|
|
||||||
/investigation/Maps
|
|
||||||
/investigation/Obj
|
|
||||||
|
45
Makefile
45
Makefile
@@ -4,37 +4,40 @@ GOBUILD ?= go build -tags ebitengl
|
|||||||
|
|
||||||
all: loader ordoor palette-idx view-ani view-font view-obj view-map view-menu view-minimap view-set
|
all: loader ordoor palette-idx view-ani view-font view-obj view-map view-menu view-minimap view-set
|
||||||
|
|
||||||
loader: $(srcfiles)
|
bin:
|
||||||
$(GOBUILD) -o loader ./cmd/loader
|
mkdir bin
|
||||||
|
|
||||||
palette-idx: $(srcfiles)
|
loader: bin $(srcfiles)
|
||||||
$(GOBUILD) -o palette-idx ./cmd/palette-idx
|
$(GOBUILD) -o bin/loader ./cmd/loader
|
||||||
|
|
||||||
view-ani: $(srcfiles)
|
palette-idx: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-ani ./cmd/view-ani
|
$(GOBUILD) -o bin/palette-idx ./cmd/palette-idx
|
||||||
|
|
||||||
view-font: $(srcfiles)
|
view-ani: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-font ./cmd/view-font
|
$(GOBUILD) -o bin/view-ani ./cmd/view-ani
|
||||||
|
|
||||||
view-obj: $(srcfiles)
|
view-font: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-obj ./cmd/view-obj
|
$(GOBUILD) -o bin/view-font ./cmd/view-font
|
||||||
|
|
||||||
view-map: $(srcfiles)
|
view-obj: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-map ./cmd/view-map
|
$(GOBUILD) -o bin/view-obj ./cmd/view-obj
|
||||||
|
|
||||||
view-menu: $(srcfiles)
|
view-map: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-menu ./cmd/view-menu
|
$(GOBUILD) -o bin/view-map ./cmd/view-map
|
||||||
|
|
||||||
view-minimap: $(srcfiles)
|
view-menu: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-minimap ./cmd/view-minimap
|
$(GOBUILD) -o bin/view-menu ./cmd/view-menu
|
||||||
|
|
||||||
view-set: $(srcfiles)
|
view-minimap: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-set ./cmd/view-set
|
$(GOBUILD) -o bin/view-minimap ./cmd/view-minimap
|
||||||
|
|
||||||
ordoor: $(srcfiles)
|
view-set: bin $(srcfiles)
|
||||||
$(GOBUILD) -o ordoor ./cmd/ordoor
|
$(GOBUILD) -o bin/view-set ./cmd/view-set
|
||||||
|
|
||||||
|
ordoor: bin $(srcfiles)
|
||||||
|
$(GOBUILD) -o bin/ordoor ./cmd/ordoor
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f loader ordoor view-ani view-obj view-map view-minimap view-set palette-idx view-font
|
rm -rf bin
|
||||||
|
|
||||||
.PHONY: all clean
|
.PHONY: all clean
|
||||||
|
82
README.md
82
README.md
@@ -1,20 +1,34 @@
|
|||||||
# Ordoor
|
# Ordoor
|
||||||
|
|
||||||
Ordoor is an **unofficial** [game engine recreation](https://en.wikipedia.org/wiki/Game_engine_recreation)
|
Ordoor is an **unofficial** [game engine recreation](https://en.wikipedia.org/wiki/Game_engine_recreation)
|
||||||
of the classic game from 1998, [Warhammer 40,000: Chaos Gate](https://en.wikipedia.org/wiki/Warhammer_40,000:_Chaos_Gate)
|
of the Random Games, Inc., [Strategy Engine](https://www.mobygames.com/game-group/game-engine-random-games-1996-2000-strategy-engine),
|
||||||
|
which was in use from 1996 - 2000.
|
||||||
|
|
||||||
**You must have a copy of the original game data to use this project**. GOG is
|
Four games are known to have been published for this engine:
|
||||||
the current publisher of this game; [you can purchase it here](https://www.gog.com/game/warhammer_40000_chaos_gate).
|
|
||||||
|
|
||||||
"Warhammer 40,000" is a trademark of Games Workshop, and the game data used by
|
* [Wages of War: The Business of Battle](https://en.wikipedia.org/wiki/Wages_of_War) (1996)
|
||||||
Ordoor contains Games Workshop intellectual property. I am confident that this
|
* [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War) (1998)
|
||||||
project uses all those things in accordance with the
|
* [Warhammer 40,000: Chaos Gate](https://en.wikipedia.org/wiki/Warhammer_40,000:_Chaos_Gate) (1998) [GOG](https://www.gog.com/game/warhammer_40000_chaos_gate)
|
||||||
[Intellectual Property Policy](https://www.games-workshop.com/en-GB/Intellectual-Property-Policy)
|
* [Avalon Hill's Squad Leader](https://en.wikipedia.org/wiki/Avalon_Hill%27s_Squad_Leader) (2000)
|
||||||
and the license granted when purchasing a copy of the game in question. Do let
|
|
||||||
me know if you see or suspect any violation, and I'll address it immediately.
|
The aim of Ordoor is to be a complete reimplementation that allows all four
|
||||||
|
of these games to be played on modern hardware. It should also permit new games
|
||||||
|
of the same style to be built.
|
||||||
|
|
||||||
|
For each of the games above, **You must have a copy of the original game data to play**.
|
||||||
|
Links are provided above if we're aware of an active publisher; otherwise, check
|
||||||
|
your back catalogue, or perhaps a local charity shop.
|
||||||
|
|
||||||
|
Trademarks and intellectual property are the property of their respective
|
||||||
|
owners, and the games mentioned above (including the game data) are protected by
|
||||||
|
copyright. As a mere game engine recreation, we're confident that this project
|
||||||
|
operates legally, and that its goal is a noble one. Do get in touch if you
|
||||||
|
believe otherwise!
|
||||||
|
|
||||||
Ordoor is a portmanteau of Order Door, which is, of course, the opposite of a
|
Ordoor is a portmanteau of Order Door, which is, of course, the opposite of a
|
||||||
Chaos Gate.
|
Chaos Gate. The project began with a Chaos Gate recreation, then more games were
|
||||||
|
discovered, so scope expanded. A rename and/or rewrite may be on the cards as a
|
||||||
|
result.
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
@@ -31,6 +45,27 @@ I've just been informed that another game from 1998, [Soldiers At War](https://e
|
|||||||
seems to use the same engine. Maybe at some point Ordoor will be able to play
|
seems to use the same engine. Maybe at some point Ordoor will be able to play
|
||||||
both. Will that need a rename? Hmm. Watch this space.
|
both. Will that need a rename? Hmm. Watch this space.
|
||||||
|
|
||||||
|
## Long-term goals
|
||||||
|
|
||||||
|
Once full playthrough of the official single-player campaign for all four games
|
||||||
|
has been achieved, thoughts turn to other things we could do. Here are some
|
||||||
|
ideas, mostly at random.
|
||||||
|
|
||||||
|
Multi-player support.
|
||||||
|
|
||||||
|
Graphics enhancements - 3D models instead of sprites, high-resolution tile sets,
|
||||||
|
32-bit colour, etc. Hopefully we'd be able to drop these in one at a time.
|
||||||
|
|
||||||
|
Vastly improved AI.
|
||||||
|
|
||||||
|
Mash-ups? How do mercenaries fare against cultists fare against Nazis? Only one
|
||||||
|
way to find out!
|
||||||
|
|
||||||
|
New campaigns with existing assets. Tell new stories, or elaborate on / modify
|
||||||
|
existing ones.
|
||||||
|
|
||||||
|
Completely new fantasy game using the same engine.
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
I'm writing code in Go at the moment, so you'll need to have a Go runtime
|
I'm writing code in Go at the moment, so you'll need to have a Go runtime
|
||||||
@@ -119,30 +154,3 @@ $ ./scripts/convert-wav ./orig/Wav
|
|||||||
As with video playback, the ambition is to *eventually* remove this dependency
|
As with video playback, the ambition is to *eventually* remove this dependency
|
||||||
and operate on the unmodified files instead.
|
and operate on the unmodified files instead.
|
||||||
|
|
||||||
## Miscellany
|
|
||||||
|
|
||||||
"Mission Setup" includes information about available squad types
|
|
||||||
|
|
||||||
From EquipDef.cpp Dumo: CEquipment we learn the following object types:
|
|
||||||
|
|
||||||
0. DELETED
|
|
||||||
1. WEAPON
|
|
||||||
2. GRENADE
|
|
||||||
3. MEDIPACK
|
|
||||||
4. SCANNER
|
|
||||||
5. GENESEED
|
|
||||||
6. CLIP
|
|
||||||
7. DOOR KEY
|
|
||||||
8. DOOR KEY
|
|
||||||
9. DOOR KEY
|
|
||||||
10. DOOR KEY
|
|
||||||
|
|
||||||
And we learn they can be "on"....
|
|
||||||
|
|
||||||
0. CHARACTER
|
|
||||||
1. VEHICLE
|
|
||||||
2. CANISTER
|
|
||||||
|
|
||||||
I'm starting to see some parallels with [this](https://github.com/shlainn/game-file-formats/wiki/)
|
|
||||||
in the data formats, and the timeline (1997) seems about right. Worth keeping an
|
|
||||||
eye on!
|
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/data"
|
"code.ur.gs/lupine/ordoor/internal/data"
|
||||||
"code.ur.gs/lupine/ordoor/internal/fonts"
|
"code.ur.gs/lupine/ordoor/internal/fonts"
|
||||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||||
@@ -16,29 +17,38 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
|
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FIXME: all these paths are hardcoded with Chaos Gate in mind
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
loadData()
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
engine := cfg.DefaultEngine()
|
||||||
|
gamePath := engine.DataDir
|
||||||
|
|
||||||
|
loadData(filepath.Join(gamePath, "Data"))
|
||||||
|
|
||||||
if !*skipObj {
|
if !*skipObj {
|
||||||
loadObj()
|
loadObj(filepath.Join(gamePath, "Obj"))
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMapsFrom("Maps")
|
loadMapsFrom(filepath.Join(gamePath, "Maps"))
|
||||||
loadMapsFrom("MultiMaps")
|
loadMapsFrom(filepath.Join(gamePath, "MultiMaps"))
|
||||||
loadSets()
|
loadSets(filepath.Join(gamePath, "Sets"))
|
||||||
loadMenus()
|
loadMenus(filepath.Join(gamePath, "Menu"))
|
||||||
loadFonts()
|
loadFonts(filepath.Join(gamePath, "Fonts"))
|
||||||
loadIdx()
|
loadIdx(filepath.Join(gamePath, "Idx", "WarHammer.idx"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadData() {
|
func loadData(dataPath string) {
|
||||||
dataPath := filepath.Join(*gamePath, "Data")
|
|
||||||
accountingPath := filepath.Join(dataPath, "Accounting.dat")
|
accountingPath := filepath.Join(dataPath, "Accounting.dat")
|
||||||
aniObDefPath := filepath.Join(dataPath, "AniObDef.dat")
|
aniObDefPath := filepath.Join(dataPath, "AniObDef.dat")
|
||||||
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
|
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
|
||||||
@@ -84,9 +94,7 @@ func loadData() {
|
|||||||
ha.Print()
|
ha.Print()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadObj() {
|
func loadObj(objDataPath string) {
|
||||||
objDataPath := filepath.Join(*gamePath, "Obj")
|
|
||||||
|
|
||||||
// TODO: Obj/cpiece.rec isn't loaded by this. Do we need it? How do we know?
|
// TODO: Obj/cpiece.rec isn't loaded by this. Do we need it? How do we know?
|
||||||
log.Printf("Loading %s...", objDataPath)
|
log.Printf("Loading %s...", objDataPath)
|
||||||
objects, err := data.LoadObjects(objDataPath)
|
objects, err := data.LoadObjects(objDataPath)
|
||||||
@@ -116,8 +124,7 @@ func loadObj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMapsFrom(part string) {
|
func loadMapsFrom(mapsPath string) {
|
||||||
mapsPath := filepath.Join(*gamePath, part)
|
|
||||||
log.Printf("Loading maps from %s", mapsPath)
|
log.Printf("Loading maps from %s", mapsPath)
|
||||||
|
|
||||||
gameMaps, err := maps.LoadGameMaps(mapsPath)
|
gameMaps, err := maps.LoadGameMaps(mapsPath)
|
||||||
@@ -140,8 +147,7 @@ func loadMapsFrom(part string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSets() {
|
func loadSets(setsPath string) {
|
||||||
setsPath := filepath.Join(*gamePath, "Sets")
|
|
||||||
log.Printf("Loading sets from %s", setsPath)
|
log.Printf("Loading sets from %s", setsPath)
|
||||||
|
|
||||||
mapSets, err := sets.LoadSets(setsPath)
|
mapSets, err := sets.LoadSets(setsPath)
|
||||||
@@ -157,8 +163,8 @@ func loadSets() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMenus() {
|
func loadMenus(menusPath string) {
|
||||||
menusPath := filepath.Join(*gamePath, "Menu")
|
log.Printf("Loading menus from %s", menusPath)
|
||||||
|
|
||||||
menus, err := menus.LoadMenus(menusPath)
|
menus, err := menus.LoadMenus(menusPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -187,8 +193,8 @@ func displayRecord(record *menus.Record, depth int) {
|
|||||||
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
|
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFonts() {
|
func loadFonts(fontsPath string) {
|
||||||
fontsPath := filepath.Join(*gamePath, "Fonts")
|
log.Printf("Loading fonts from %s", fontsPath)
|
||||||
|
|
||||||
fonts, err := fonts.LoadFonts(fontsPath)
|
fonts, err := fonts.LoadFonts(fontsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -200,8 +206,9 @@ func loadFonts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadIdx() {
|
func loadIdx(idxPath string) {
|
||||||
idxPath := filepath.Join(*gamePath, "Idx", "WarHammer.idx")
|
log.Printf("Loading idx from %s", idxPath)
|
||||||
|
|
||||||
idx, err := idx.Load(idxPath)
|
idx, err := idx.Load(idxPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse %s as idx: %v", idxPath, err)
|
log.Fatalf("Failed to parse %s as idx: %v", idxPath, err)
|
||||||
|
@@ -11,11 +11,14 @@ import (
|
|||||||
"golang.org/x/image/colornames"
|
"golang.org/x/image/colornames"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
groupIdx = flag.Int("group", 1, "Group index to start at")
|
groupIdx = flag.Int("group", 1, "Group index to start at")
|
||||||
recIdx = flag.Int("record", 0, "Record index to start at")
|
recIdx = flag.Int("record", 0, "Record index to start at")
|
||||||
|
|
||||||
@@ -43,12 +46,17 @@ type state struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" {
|
if *configFile == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to set up asset store: %v", err)
|
log.Fatal("Failed to set up asset store: %v", err)
|
||||||
}
|
}
|
||||||
|
@@ -10,11 +10,14 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
fontName = flag.String("font", "", "Name of a font, e.g., basfont12")
|
fontName = flag.String("font", "", "Name of a font, e.g., basfont12")
|
||||||
txt = flag.String("text", "Test string", "Text to render")
|
txt = flag.String("text", "Test string", "Text to render")
|
||||||
|
|
||||||
@@ -37,12 +40,17 @@ type state struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || *fontName == "" {
|
if *configFile == "" || *fontName == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@@ -9,13 +9,16 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/scenario"
|
"code.ur.gs/lupine/ordoor/internal/scenario"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
|
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
|
||||||
|
|
||||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||||
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
||||||
@@ -28,14 +31,19 @@ type env struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || *gameMap == "" {
|
if *configFile == "" || *gameMap == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to scan root directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scenario, err := scenario.NewScenario(assets, *gameMap)
|
scenario, err := scenario.NewScenario(assets, *gameMap)
|
||||||
|
@@ -8,11 +8,14 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
|
menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
|
||||||
|
|
||||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||||
@@ -28,12 +31,17 @@ type dlg struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || *menuName == "" {
|
if *configFile == "" || *menuName == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@@ -10,14 +10,17 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
|
|
||||||
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
|
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
|
||||||
|
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
|
||||||
|
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
|
||||||
|
|
||||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||||
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
||||||
@@ -42,12 +45,17 @@ type state struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || (*objName == "" && *objFile == "") {
|
if *configFile == "" || (*objName == "" && *objFile == "") {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to set up asset store: %v", err)
|
log.Fatal("Failed to set up asset store: %v", err)
|
||||||
}
|
}
|
||||||
|
@@ -10,12 +10,15 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
setName = flag.String("set", "", "Name of a set, e.g., map01")
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
|
setName = flag.String("set", "", "Name of a set, e.g., map01")
|
||||||
|
|
||||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||||
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
||||||
@@ -39,12 +42,17 @@ type state struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || *setName == "" {
|
if *configFile == "" || *setName == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,21 @@
|
|||||||
[ordoor]
|
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
|
||||||
data_dir = "./orig"
|
|
||||||
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
|
default_engine = "ordoor"
|
||||||
|
|
||||||
|
[engines.geas] # Wages of War -> Gifts of Peace -> Geas
|
||||||
|
data_dir = "./WoW"
|
||||||
|
|
||||||
|
[engines.ordoor] # Chaos Gate -> Order Door -> Ordoor
|
||||||
|
data_dir = "./CG"
|
||||||
|
palette = "ChaosGate"
|
||||||
|
|
||||||
|
[engines.baps] # Soldiers At War -> Boys at Play -> Baps
|
||||||
|
data_dir = "./SaW"
|
||||||
|
palette = "SoldiersAtWar"
|
||||||
|
|
||||||
|
# [engines.] Squad Leader -> ??? -> ???
|
||||||
|
# data_dir = "./SL"
|
||||||
|
# palette = "SquadLeader" # may not be relevant?
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
play_movies = true
|
play_movies = true
|
||||||
|
@@ -7,19 +7,16 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/data"
|
"code.ur.gs/lupine/ordoor/internal/data"
|
||||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
RootDir = "" // Used in the entryMap for entries pertaining to the root dir
|
|
||||||
)
|
|
||||||
|
|
||||||
type entryMap map[string]map[string]string
|
type entryMap map[string]map[string]string
|
||||||
|
|
||||||
// type AssetStore is responsible for lazily loading game data when it is
|
// type AssetStore is responsible for lazily loading game data when it is
|
||||||
// required. Applications shouldn't need to do anything except set one of these
|
// required. Applications shouldn't need to do anything except set one of these
|
||||||
// up, pointing at the game dir root, to access all assets.
|
// up, pointing at the game dir root, to access all assets for that game.
|
||||||
//
|
//
|
||||||
// Assets should be loaded on-demand to keep memory costs as low as possible.
|
// Assets should be loaded on-demand to keep memory costs as low as possible.
|
||||||
// Cross-platform differences such as filename case sensitivity are also dealt
|
// Cross-platform differences such as filename case sensitivity are also dealt
|
||||||
@@ -27,8 +24,12 @@ type entryMap map[string]map[string]string
|
|||||||
//
|
//
|
||||||
// We assume the directory is read-only. You can run Refresh() if you make a
|
// We assume the directory is read-only. You can run Refresh() if you make a
|
||||||
// change.
|
// change.
|
||||||
|
//
|
||||||
|
// To mix assets from different games, either construct a synthetic directory
|
||||||
|
// or instantiate two separate asset stores.
|
||||||
type AssetStore struct {
|
type AssetStore struct {
|
||||||
RootDir string
|
RootDir string
|
||||||
|
// Palette
|
||||||
|
|
||||||
// Case-insensitive file lookup.
|
// Case-insensitive file lookup.
|
||||||
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
|
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
|
||||||
@@ -50,9 +51,13 @@ type AssetStore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new AssetStore
|
// New returns a new AssetStore
|
||||||
func New(dir string) (*AssetStore, error) {
|
func New(engine *config.Engine) (*AssetStore, error) {
|
||||||
|
if engine == nil {
|
||||||
|
return nil, fmt.Errorf("Unconfigured engine passed to assetstore")
|
||||||
|
}
|
||||||
|
|
||||||
store := &AssetStore{
|
store := &AssetStore{
|
||||||
RootDir: dir,
|
RootDir: engine.DataDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill entryMap
|
// fill entryMap
|
||||||
@@ -70,7 +75,7 @@ func (a *AssetStore) Refresh() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newEntryMap := make(entryMap, len(rootEntries))
|
newEntryMap := make(entryMap, len(rootEntries))
|
||||||
newEntryMap[RootDir] = rootEntries
|
newEntryMap[""] = rootEntries
|
||||||
|
|
||||||
for lower, natural := range rootEntries {
|
for lower, natural := range rootEntries {
|
||||||
path := filepath.Join(a.RootDir, natural)
|
path := filepath.Join(a.RootDir, natural)
|
||||||
@@ -118,7 +123,7 @@ func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
|
|||||||
dir = canonical(dir)
|
dir = canonical(dir)
|
||||||
if base, ok := a.entries[dir]; ok {
|
if base, ok := a.entries[dir]; ok {
|
||||||
if file, ok := base[filename]; ok {
|
if file, ok := base[filename]; ok {
|
||||||
actualDir := a.entries[RootDir][dir]
|
actualDir := a.entries[""][dir]
|
||||||
return filepath.Join(a.RootDir, actualDir, file), nil
|
return filepath.Join(a.RootDir, actualDir, file), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,15 +2,16 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ordoor struct {
|
type Engine struct {
|
||||||
DataDir string `toml:"data_dir"`
|
DataDir string `toml:"data_dir"`
|
||||||
VideoPlayer []string `toml:"video_player"`
|
Palette string `toml:"palette"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Things set in the options hash
|
// Things set in the options hash
|
||||||
@@ -38,12 +39,40 @@ type Options struct {
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
filename string `toml:"-"`
|
filename string `toml:"-"`
|
||||||
|
|
||||||
|
VideoPlayer []string `toml:"video_player"`
|
||||||
|
Engines map[string]Engine `toml:"engines"`
|
||||||
|
|
||||||
|
DefaultEngineName string `toml:"default_engine"`
|
||||||
|
|
||||||
|
// FIXME: options may well end up being per-engine too
|
||||||
Defaults *Options `toml:"-"`
|
Defaults *Options `toml:"-"`
|
||||||
Ordoor `toml:"ordoor"`
|
|
||||||
Options `toml:"options"`
|
Options `toml:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(filename string) (*Config, error) {
|
func (c *Config) Engine(name string) *Engine {
|
||||||
|
engine, ok := c.Engines[name]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) DefaultEngine() *Engine {
|
||||||
|
return c.Engine(c.DefaultEngineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: case-insensitive lookup
|
||||||
|
func (c *Config) DataFile(engine string, path string) string {
|
||||||
|
cfg, ok := c.Engines[engine]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(cfg.DataDir, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(filename string, overrideDefaultEngine string) (*Config, error) {
|
||||||
var out Config
|
var out Config
|
||||||
|
|
||||||
_, err := toml.DecodeFile(filename, &out)
|
_, err := toml.DecodeFile(filename, &out)
|
||||||
@@ -53,7 +82,15 @@ func Load(filename string) (*Config, error) {
|
|||||||
|
|
||||||
out.filename = filename
|
out.filename = filename
|
||||||
|
|
||||||
return &out, err
|
if overrideDefaultEngine != "" {
|
||||||
|
out.DefaultEngineName = overrideDefaultEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.DefaultEngine() == nil {
|
||||||
|
return nil, fmt.Errorf("Default engine %q not configured", out.DefaultEngineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) HasUnsetOptions() bool {
|
func (c *Config) HasUnsetOptions() bool {
|
||||||
@@ -72,11 +109,6 @@ func (c *Config) Save() error {
|
|||||||
return toml.NewEncoder(f).Encode(c)
|
return toml.NewEncoder(f).Encode(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: case-insensitive lookup
|
|
||||||
func (c *Config) DataFile(path string) string {
|
|
||||||
return filepath.Join(c.DataDir, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ResetDefaults() error {
|
func (c *Config) ResetDefaults() error {
|
||||||
if c.Defaults == nil {
|
if c.Defaults == nil {
|
||||||
return errors.New("Defaults not available")
|
return errors.New("Defaults not available")
|
||||||
|
@@ -34,12 +34,12 @@ type Ordoor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Run(configFile string, overrideX, overrideY int) error {
|
func Run(configFile string, overrideX, overrideY int) error {
|
||||||
cfg, err := config.Load(configFile)
|
cfg, err := config.Load(configFile, "ordoor")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Couldn't load config file: %v", err)
|
return fmt.Errorf("Couldn't load config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(cfg.Ordoor.DataDir)
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to initialize asset store: %v", err)
|
return fmt.Errorf("Failed to initialize asset store: %v", err)
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (o *Ordoor) PlayVideo(name string, skippable bool) {
|
func (o *Ordoor) PlayVideo(name string, skippable bool) {
|
||||||
filename := o.config.DataFile("SMK/" + name + ".smk")
|
filename := o.config.DataFile("ordoor", "SMK/"+name+".smk")
|
||||||
|
|
||||||
if len(o.config.VideoPlayer) == 0 {
|
if len(o.config.VideoPlayer) == 0 {
|
||||||
log.Printf("Video player not configured, skipping video %v", filename)
|
log.Printf("Video player not configured, skipping video %v", filename)
|
||||||
|
Reference in New Issue
Block a user