Start reorganising for multiple games

This commit is contained in:
2020-06-01 01:08:53 +01:00
parent 59baf20c35
commit c1268e8d57
15 changed files with 257 additions and 145 deletions

18
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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!

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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
} }
} }

View File

@@ -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")

View File

@@ -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)
} }

View File

@@ -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)