Compare commits

...

3 Commits

Author SHA1 Message Date
3866ee07a8 Source the palette name from data 2020-06-01 01:24:44 +01:00
c1268e8d57 Start reorganising for multiple games 2020-06-01 01:08:53 +01:00
59baf20c35 Remove unneeded palette investigation 2020-06-01 00:46:02 +01:00
28 changed files with 291 additions and 997 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

@@ -3,42 +3,58 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"image/color"
"log" "log"
"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"
"code.ur.gs/lupine/ordoor/internal/maps" "code.ur.gs/lupine/ordoor/internal/maps"
"code.ur.gs/lupine/ordoor/internal/menus" "code.ur.gs/lupine/ordoor/internal/menus"
"code.ur.gs/lupine/ordoor/internal/palettes"
"code.ur.gs/lupine/ordoor/internal/sets" "code.ur.gs/lupine/ordoor/internal/sets"
) )
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 {
if !*skipObj { log.Fatalf("Failed to load config: %v", err)
loadObj() }
engine := cfg.DefaultEngine()
gamePath := engine.DataDir
palette, ok := palettes.Get(engine.Palette)
if !ok {
log.Fatalf("Unknown palette name: %v", engine.Palette)
} }
loadMapsFrom("Maps") loadData(filepath.Join(gamePath, "Data"))
loadMapsFrom("MultiMaps")
loadSets() if !*skipObj {
loadMenus() loadObj(filepath.Join(gamePath, "Obj"))
loadFonts() }
loadIdx()
loadMapsFrom(filepath.Join(gamePath, "Maps"))
loadMapsFrom(filepath.Join(gamePath, "MultiMaps"))
loadSets(filepath.Join(gamePath, "Sets"))
loadMenus(filepath.Join(gamePath, "Menu"), palette)
loadFonts(filepath.Join(gamePath, "Fonts"))
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 +100,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 +130,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 +153,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,10 +169,10 @@ func loadSets() {
} }
} }
func loadMenus() { func loadMenus(menusPath string, palette color.Palette) {
menusPath := filepath.Join(*gamePath, "Menu") log.Printf("Loading menus from %s", menusPath)
menus, err := menus.LoadMenus(menusPath) menus, err := menus.LoadMenus(menusPath, palette)
if err != nil { if err != nil {
log.Fatalf("Failed to parse %s/*.mnu as menus: %v", menusPath, err) log.Fatalf("Failed to parse %s/*.mnu as menus: %v", menusPath, err)
} }
@@ -187,8 +199,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 +212,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,22 @@
[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"
palette = "WagesOfWar"
[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

@@ -2,24 +2,23 @@ package assetstore
import ( import (
"fmt" "fmt"
"image/color"
"io/ioutil" "io/ioutil"
"os" "os"
"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"
) "code.ur.gs/lupine/ordoor/internal/palettes"
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 +26,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 color.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 +53,19 @@ 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")
}
palette, ok := palettes.Get(engine.Palette)
if !ok {
return nil, fmt.Errorf("Couldn't find palette %q for engine", engine.Palette)
}
store := &AssetStore{ store := &AssetStore{
RootDir: dir, RootDir: engine.DataDir,
Palette: palette,
} }
// fill entryMap // fill entryMap
@@ -70,7 +83,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 +131,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

@@ -62,7 +62,7 @@ func (a *AssetStore) Menu(name string) (*Menu, error) {
return nil, err return nil, err
} }
raw, err := menus.LoadMenu(filename) raw, err := menus.LoadMenu(filename, a.Palette)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -117,7 +117,7 @@ func (o *Object) Sprite(idx int) (*Sprite, error) {
} }
raw := o.raw.Sprites[idx] raw := o.raw.Sprites[idx]
img, err := ebiten.NewImageFromImage(raw.ToImage(), ebiten.FilterDefault) img, err := ebiten.NewImageFromImage(raw.ToImage(o.assets.Palette), ebiten.FilterDefault)
if err != nil { if err != nil {
return nil, err return nil, err
} }

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

@@ -4,6 +4,7 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"image" "image"
"image/color"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@@ -12,7 +13,6 @@ import (
"strings" "strings"
"code.ur.gs/lupine/ordoor/internal/data/rle" "code.ur.gs/lupine/ordoor/internal/data/rle"
"code.ur.gs/lupine/ordoor/internal/palettes"
) )
type SpriteHeader struct { type SpriteHeader struct {
@@ -53,12 +53,12 @@ type Sprite struct {
Data []byte Data []byte
} }
func (s *Sprite) ToImage() *image.Paletted { func (s *Sprite) ToImage(palette color.Palette) *image.Paletted {
return &image.Paletted{ return &image.Paletted{
Pix: s.Data, Pix: s.Data,
Stride: int(s.Width), Stride: int(s.Width),
Rect: image.Rect(0, 0, int(s.Width), int(s.Height)), Rect: image.Rect(0, 0, int(s.Width), int(s.Height)),
Palette: palettes.DefaultPalette(), Palette: palette,
} }
} }

View File

@@ -9,7 +9,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"code.ur.gs/lupine/ordoor/internal/palettes"
"code.ur.gs/lupine/ordoor/internal/util/asciiscan" "code.ur.gs/lupine/ordoor/internal/util/asciiscan"
) )
@@ -143,7 +142,7 @@ func (p *Properties) Point() image.Point {
return image.Point{} return image.Point{}
} }
func LoadMenu(filename string) (*Menu, error) { func LoadMenu(filename string, palette color.Palette) (*Menu, error) {
name := filepath.Base(filename) name := filepath.Base(filename)
name = strings.TrimSuffix(name, filepath.Ext(name)) name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.ToLower(name) name = strings.ToLower(name)
@@ -163,7 +162,7 @@ func LoadMenu(filename string) (*Menu, error) {
return nil, err return nil, err
} }
if err := loadProperties(out, scanner); err != nil { if err := loadProperties(out, scanner, palette); err != nil {
return nil, err return nil, err
} }
@@ -189,7 +188,7 @@ func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error {
return nil return nil
} }
func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error { func loadProperties(menu *Menu, scanner *asciiscan.Scanner, palette color.Palette) error {
for { for {
ok, err := scanner.PeekProperty() ok, err := scanner.PeekProperty()
@@ -218,9 +217,9 @@ func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
switch strings.ToUpper(k) { switch strings.ToUpper(k) {
case "BACKGROUND COLOR": case "BACKGROUND COLOR":
menu.BackgroundColor = palettes.DefaultPalette()[vInt] menu.BackgroundColor = palette[vInt]
case "HYPERTEXT COLOR": case "HYPERTEXT COLOR":
menu.HypertextColor = palettes.DefaultPalette()[vInt] menu.HypertextColor = palette[vInt]
case "FONT TYPE": case "FONT TYPE":
menu.FontType = vInt menu.FontType = vInt
default: default:
@@ -319,7 +318,7 @@ func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
return nil return nil
} }
func LoadMenus(dir string) (map[string]*Menu, error) { func LoadMenus(dir string, palette color.Palette) (map[string]*Menu, error) {
fis, err := ioutil.ReadDir(dir) fis, err := ioutil.ReadDir(dir)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -337,7 +336,7 @@ func LoadMenus(dir string) (map[string]*Menu, error) {
continue continue
} }
built, err := LoadMenu(filepath.Join(dir, relname)) built, err := LoadMenu(filepath.Join(dir, relname), palette)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err) return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err)
} }

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)

View File

@@ -5,9 +5,6 @@ import (
"sync" "sync"
) )
// Override this to change the palette globally
const DefaultPaletteName = "ChaosGate"
var ( var (
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0} Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
@@ -16,6 +13,8 @@ var (
initPalettes = sync.Once{} initPalettes = sync.Once{}
) )
func DefaultPalette() color.Palette { func Get(name string) (color.Palette, bool) {
return Palettes[DefaultPaletteName] p, ok := Palettes[name]
return p, ok
} }

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env ruby
lines = File.read("palette.ppm").split("\n")
lines = lines.select { |l| l[0] != "#" }
raise "Not an ASCII ppm" if lines.shift != "P3"
raise "Incorrect dimensions" unless lines.shift == "1 256"
raise "Incorrect maxval" unless lines.shift == "255"
raise "Too many lines left" unless lines.size == 768
puts <<EOF
package data
import "image/color"
var (
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
ColorPalette = color.Palette{
Transparent,
EOF
lines.shift(3) # Ignore idx 0
255.times do
r, g, b = lines.shift(3).map(&:to_i)
puts "\t\tcolor.RGBA{R: #{r}, G: #{g}, B: #{b}, A: 255},"
end
puts <<EOF
}
)
EOF

View File

@@ -1,12 +0,0 @@
# jungle floor
# jungtil.obj/.asn
# /--> d:\warflics\missions\jungtil.flc
#
0:DEF 2;
0:TYPE 2;
END OF FILE

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,772 +0,0 @@
P3
# CREATOR: GIMP PNM Filter Version 1.1
1 256
255
255
255
255
128
0
0
0
128
0
128
128
0
0
0
128
128
0
128
0
128
128
192
192
192
192
220
192
166
202
240
255
255
255
240
240
240
221
221
221
203
203
203
187
187
187
178
178
178
168
168
168
157
157
157
147
147
147
137
137
137
127
127
127
117
117
117
106
106
106
96
96
96
86
86
86
76
76
76
61
61
61
49
49
49
36
36
36
24
24
24
12
12
12
0
0
0
134
134
255
113
113
241
93
93
228
72
72
214
63
63
200
55
55
186
46
46
172
38
38
158
29
29
144
20
20
131
12
12
117
3
3
103
3
3
91
3
3
79
3
3
68
3
3
56
255
145
145
242
123
123
230
101
101
217
79
79
205
70
70
193
61
61
181
53
53
169
44
44
157
35
35
144
26
26
132
18
18
120
9
9
108
8
8
94
7
7
79
7
7
65
6
6
147
142
185
132
126
172
117
109
159
102
93
146
95
86
133
88
78
123
82
73
115
77
67
107
72
61
100
67
55
92
61
50
84
56
44
76
51
38
68
46
32
60
40
27
52
35
21
44
200
150
137
187
130
115
175
110
94
164
95
77
154
79
61
143
64
44
137
60
42
132
55
38
125
50
33
120
48
29
111
44
26
103
39
24
94
35
21
83
30
18
72
25
14
61
20
10
121
107
34
109
94
29
96
82
25
84
69
20
77
62
17
70
55
14
63
47
12
56
40
9
93
120
53
80
103
42
66
86
31
53
69
20
49
60
16
45
52
12
43
44
10
43
38
8
136
145
44
118
128
37
101
111
30
83
94
23
70
79
17
56
65
11
42
50
6
28
36
0
57
134
64
48
118
54
38
101
43
29
85
33
22
71
25
15
58
17
7
45
8
0
32
0
143
87
56
126
75
45
110
64
35
93
52
24
85
44
16
72
36
12
64
32
8
56
24
4
127
96
54
115
85
46
102
75
39
90
64
31
82
57
25
75
51
20
68
44
15
61
38
10
141
86
56
126
75
46
110
65
36
95
54
26
88
51
25
73
40
18
57
29
10
42
18
3
172
199
199
138
173
173
104
148
148
71
122
122
37
97
97
3
71
71
4
56
56
4
41
41
217
209
200
202
194
184
188
178
167
173
163
151
158
147
134
148
136
123
137
125
112
126
114
101
116
104
91
105
93
80
94
82
69
84
71
58
73
60
47
62
50
36
52
39
25
41
28
14
231
232
207
219
217
180
208
201
152
196
186
125
184
171
98
173
155
70
161
140
43
150
129
39
139
119
37
127
109
33
117
99
29
105
89
25
90
76
21
75
62
18
60
49
14
45
35
10
128
99
127
113
73
112
97
47
97
82
21
82
75
2
74
68
2
67
52
3
50
35
4
33
247
178
102
229
152
75
212
127
48
194
101
21
179
87
16
161
73
12
142
59
9
124
45
5
255
0
0
194
3
3
161
2
2
255
227
11
209
185
8
169
150
6
103
190
255
2
130
232
4
4
209
0
255
0
7
180
7
3
132
3
255
114
230
255
17
205
203
6
156
246
164
73
221
123
16
177
95
4
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
132
18
18
113
13
13
94
7
7
107
8
8
120
9
9
193
62
62
181
54
54
168
45
45
157
36
36
144
27
27
62
50
36
57
45
31
52
39
25
46
34
20
41
28
14
255
251
240
160
160
164
128
128
128
255
0
0
0
255
0
255
255
0
0
0
255
255
0
255
0
255
255
255
255
255

View File

@@ -1,13 +0,0 @@
To build this palette, I did the following:
* Created the .asn + .obj files by hand
* Referenced them in a .set file and asked WH40K_TD.exe to render the sprites
* Screenshotted the output and used GIMP to create a 1x255 image with them all
* Exported that image as an ASCII .ppm file
* Used the `generate_palette` script to output Go code \o/
Note that palette index 0 is ignored, and hardcoded to be transparent black.
This is because 0x00 seems to be used as a record separator in .obj files, so
can't be used as a palette index anyway.

Binary file not shown.