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
/loader
/orig
/palette-idx
/view-ani
/view-font
/view-obj
/view-map
/view-minimap
/view-menu
/view-set
/ordoor
/investigation/Maps
/investigation/Obj
/isos
/CG
/SL
/SaW
/WoW
/bin

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
loader: $(srcfiles)
$(GOBUILD) -o loader ./cmd/loader
bin:
mkdir bin
palette-idx: $(srcfiles)
$(GOBUILD) -o palette-idx ./cmd/palette-idx
loader: bin $(srcfiles)
$(GOBUILD) -o bin/loader ./cmd/loader
view-ani: $(srcfiles)
$(GOBUILD) -o view-ani ./cmd/view-ani
palette-idx: bin $(srcfiles)
$(GOBUILD) -o bin/palette-idx ./cmd/palette-idx
view-font: $(srcfiles)
$(GOBUILD) -o view-font ./cmd/view-font
view-ani: bin $(srcfiles)
$(GOBUILD) -o bin/view-ani ./cmd/view-ani
view-obj: $(srcfiles)
$(GOBUILD) -o view-obj ./cmd/view-obj
view-font: bin $(srcfiles)
$(GOBUILD) -o bin/view-font ./cmd/view-font
view-map: $(srcfiles)
$(GOBUILD) -o view-map ./cmd/view-map
view-obj: bin $(srcfiles)
$(GOBUILD) -o bin/view-obj ./cmd/view-obj
view-menu: $(srcfiles)
$(GOBUILD) -o view-menu ./cmd/view-menu
view-map: bin $(srcfiles)
$(GOBUILD) -o bin/view-map ./cmd/view-map
view-minimap: $(srcfiles)
$(GOBUILD) -o view-minimap ./cmd/view-minimap
view-menu: bin $(srcfiles)
$(GOBUILD) -o bin/view-menu ./cmd/view-menu
view-set: $(srcfiles)
$(GOBUILD) -o view-set ./cmd/view-set
view-minimap: bin $(srcfiles)
$(GOBUILD) -o bin/view-minimap ./cmd/view-minimap
ordoor: $(srcfiles)
$(GOBUILD) -o ordoor ./cmd/ordoor
view-set: bin $(srcfiles)
$(GOBUILD) -o bin/view-set ./cmd/view-set
ordoor: bin $(srcfiles)
$(GOBUILD) -o bin/ordoor ./cmd/ordoor
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

View File

@@ -1,20 +1,34 @@
# Ordoor
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
the current publisher of this game; [you can purchase it here](https://www.gog.com/game/warhammer_40000_chaos_gate).
Four games are known to have been published for this engine:
"Warhammer 40,000" is a trademark of Games Workshop, and the game data used by
Ordoor contains Games Workshop intellectual property. I am confident that this
project uses all those things in accordance with the
[Intellectual Property Policy](https://www.games-workshop.com/en-GB/Intellectual-Property-Policy)
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.
* [Wages of War: The Business of Battle](https://en.wikipedia.org/wiki/Wages_of_War) (1996)
* [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War) (1998)
* [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)
* [Avalon Hill's Squad Leader](https://en.wikipedia.org/wiki/Avalon_Hill%27s_Squad_Leader) (2000)
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
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
@@ -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
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
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
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 (
"flag"
"fmt"
"image/color"
"log"
"path/filepath"
"strings"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/fonts"
"code.ur.gs/lupine/ordoor/internal/idx"
"code.ur.gs/lupine/ordoor/internal/maps"
"code.ur.gs/lupine/ordoor/internal/menus"
"code.ur.gs/lupine/ordoor/internal/palettes"
"code.ur.gs/lupine/ordoor/internal/sets"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
configFile = flag.String("config", "config.toml", "Config file")
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() {
flag.Parse()
loadData()
if !*skipObj {
loadObj()
cfg, err := config.Load(*configFile, *engine)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
engine := cfg.DefaultEngine()
gamePath := engine.DataDir
palette, ok := palettes.Get(engine.Palette)
if !ok {
log.Fatalf("Unknown palette name: %v", engine.Palette)
}
loadMapsFrom("Maps")
loadMapsFrom("MultiMaps")
loadSets()
loadMenus()
loadFonts()
loadIdx()
loadData(filepath.Join(gamePath, "Data"))
if !*skipObj {
loadObj(filepath.Join(gamePath, "Obj"))
}
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() {
dataPath := filepath.Join(*gamePath, "Data")
func loadData(dataPath string) {
accountingPath := filepath.Join(dataPath, "Accounting.dat")
aniObDefPath := filepath.Join(dataPath, "AniObDef.dat")
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
@@ -84,9 +100,7 @@ func loadData() {
ha.Print()
}
func loadObj() {
objDataPath := filepath.Join(*gamePath, "Obj")
func loadObj(objDataPath string) {
// TODO: Obj/cpiece.rec isn't loaded by this. Do we need it? How do we know?
log.Printf("Loading %s...", objDataPath)
objects, err := data.LoadObjects(objDataPath)
@@ -116,8 +130,7 @@ func loadObj() {
}
}
func loadMapsFrom(part string) {
mapsPath := filepath.Join(*gamePath, part)
func loadMapsFrom(mapsPath string) {
log.Printf("Loading maps from %s", mapsPath)
gameMaps, err := maps.LoadGameMaps(mapsPath)
@@ -140,8 +153,7 @@ func loadMapsFrom(part string) {
}
}
func loadSets() {
setsPath := filepath.Join(*gamePath, "Sets")
func loadSets(setsPath string) {
log.Printf("Loading sets from %s", setsPath)
mapSets, err := sets.LoadSets(setsPath)
@@ -157,10 +169,10 @@ func loadSets() {
}
}
func loadMenus() {
menusPath := filepath.Join(*gamePath, "Menu")
func loadMenus(menusPath string, palette color.Palette) {
log.Printf("Loading menus from %s", menusPath)
menus, err := menus.LoadMenus(menusPath)
menus, err := menus.LoadMenus(menusPath, palette)
if err != nil {
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)
}
func loadFonts() {
fontsPath := filepath.Join(*gamePath, "Fonts")
func loadFonts(fontsPath string) {
log.Printf("Loading fonts from %s", fontsPath)
fonts, err := fonts.LoadFonts(fontsPath)
if err != nil {
@@ -200,8 +212,9 @@ func loadFonts() {
}
}
func loadIdx() {
idxPath := filepath.Join(*gamePath, "Idx", "WarHammer.idx")
func loadIdx(idxPath string) {
log.Printf("Loading idx from %s", idxPath)
idx, err := idx.Load(idxPath)
if err != nil {
log.Fatalf("Failed to parse %s as idx: %v", idxPath, err)

View File

@@ -11,11 +11,14 @@ import (
"golang.org/x/image/colornames"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui"
)
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")
recIdx = flag.Int("record", 0, "Record index to start at")
@@ -43,12 +46,17 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" {
if *configFile == "" {
flag.Usage()
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 {
log.Fatal("Failed to set up asset store: %v", err)
}

View File

@@ -10,11 +10,14 @@ import (
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui"
)
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")
txt = flag.String("text", "Test string", "Text to render")
@@ -37,12 +40,17 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" || *fontName == "" {
if *configFile == "" || *fontName == "" {
flag.Usage()
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 {
log.Fatal(err)
}

View File

@@ -9,13 +9,16 @@ import (
"github.com/hajimehoshi/ebiten"
"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/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
configFile = flag.String("config", "config.toml", "Config file")
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")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
@@ -28,14 +31,19 @@ type env struct {
func main() {
flag.Parse()
if *gamePath == "" || *gameMap == "" {
if *configFile == "" || *gameMap == "" {
flag.Usage()
os.Exit(1)
}
assets, err := assetstore.New(*gamePath)
cfg, err := config.Load(*configFile, *engine)
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)

View File

@@ -8,11 +8,14 @@ import (
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui"
)
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")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
@@ -28,12 +31,17 @@ type dlg struct {
func main() {
flag.Parse()
if *gamePath == "" || *menuName == "" {
if *configFile == "" || *menuName == "" {
flag.Usage()
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 {
log.Fatal(err)
}

View File

@@ -10,14 +10,17 @@ import (
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
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")
configFile = flag.String("config", "config.toml", "Config file")
engine = flag.String("engine", "", "Override engine to use")
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")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
@@ -42,12 +45,17 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" || (*objName == "" && *objFile == "") {
if *configFile == "" || (*objName == "" && *objFile == "") {
flag.Usage()
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 {
log.Fatal("Failed to set up asset store: %v", err)
}

View File

@@ -10,12 +10,15 @@ import (
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
setName = flag.String("set", "", "Name of a set, e.g., map01")
configFile = flag.String("config", "config.toml", "Config file")
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")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
@@ -39,12 +42,17 @@ type state struct {
func main() {
flag.Parse()
if *gamePath == "" || *setName == "" {
if *configFile == "" || *setName == "" {
flag.Usage()
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 {
log.Fatal(err)
}

View File

@@ -1,6 +1,22 @@
[ordoor]
data_dir = "./orig"
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
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]
play_movies = true

View File

@@ -2,24 +2,23 @@ package assetstore
import (
"fmt"
"image/color"
"io/ioutil"
"os"
"path/filepath"
"strings"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/idx"
)
const (
RootDir = "" // Used in the entryMap for entries pertaining to the root dir
"code.ur.gs/lupine/ordoor/internal/palettes"
)
type entryMap map[string]map[string]string
// 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
// 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.
// 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
// change.
//
// To mix assets from different games, either construct a synthetic directory
// or instantiate two separate asset stores.
type AssetStore struct {
RootDir string
Palette color.Palette
// Case-insensitive file lookup.
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
@@ -50,9 +53,19 @@ type AssetStore struct {
}
// 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{
RootDir: dir,
RootDir: engine.DataDir,
Palette: palette,
}
// fill entryMap
@@ -70,7 +83,7 @@ func (a *AssetStore) Refresh() error {
}
newEntryMap := make(entryMap, len(rootEntries))
newEntryMap[RootDir] = rootEntries
newEntryMap[""] = rootEntries
for lower, natural := range rootEntries {
path := filepath.Join(a.RootDir, natural)
@@ -118,7 +131,7 @@ func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
dir = canonical(dir)
if base, ok := a.entries[dir]; ok {
if file, ok := base[filename]; ok {
actualDir := a.entries[RootDir][dir]
actualDir := a.entries[""][dir]
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
}
raw, err := menus.LoadMenu(filename)
raw, err := menus.LoadMenu(filename, a.Palette)
if err != nil {
return nil, err
}

View File

@@ -117,7 +117,7 @@ func (o *Object) Sprite(idx int) (*Sprite, error) {
}
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 {
return nil, err
}

View File

@@ -2,15 +2,16 @@ package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
)
type Ordoor struct {
DataDir string `toml:"data_dir"`
VideoPlayer []string `toml:"video_player"`
type Engine struct {
DataDir string `toml:"data_dir"`
Palette string `toml:"palette"`
}
// Things set in the options hash
@@ -38,12 +39,40 @@ type Options struct {
type Config struct {
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:"-"`
Ordoor `toml:"ordoor"`
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
_, err := toml.DecodeFile(filename, &out)
@@ -53,7 +82,15 @@ func Load(filename string) (*Config, error) {
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 {
@@ -72,11 +109,6 @@ func (c *Config) Save() error {
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 {
if c.Defaults == nil {
return errors.New("Defaults not available")

View File

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

View File

@@ -9,7 +9,6 @@ import (
"strconv"
"strings"
"code.ur.gs/lupine/ordoor/internal/palettes"
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
)
@@ -143,7 +142,7 @@ func (p *Properties) Point() 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 = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.ToLower(name)
@@ -163,7 +162,7 @@ func LoadMenu(filename string) (*Menu, error) {
return nil, err
}
if err := loadProperties(out, scanner); err != nil {
if err := loadProperties(out, scanner, palette); err != nil {
return nil, err
}
@@ -189,7 +188,7 @@ func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error {
return nil
}
func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
func loadProperties(menu *Menu, scanner *asciiscan.Scanner, palette color.Palette) error {
for {
ok, err := scanner.PeekProperty()
@@ -218,9 +217,9 @@ func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
switch strings.ToUpper(k) {
case "BACKGROUND COLOR":
menu.BackgroundColor = palettes.DefaultPalette()[vInt]
menu.BackgroundColor = palette[vInt]
case "HYPERTEXT COLOR":
menu.HypertextColor = palettes.DefaultPalette()[vInt]
menu.HypertextColor = palette[vInt]
case "FONT TYPE":
menu.FontType = vInt
default:
@@ -319,7 +318,7 @@ func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
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)
if err != nil {
return nil, err
@@ -337,7 +336,7 @@ func LoadMenus(dir string) (map[string]*Menu, error) {
continue
}
built, err := LoadMenu(filepath.Join(dir, relname))
built, err := LoadMenu(filepath.Join(dir, relname), palette)
if err != nil {
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 {
cfg, err := config.Load(configFile)
cfg, err := config.Load(configFile, "ordoor")
if err != nil {
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 {
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) {
filename := o.config.DataFile("SMK/" + name + ".smk")
filename := o.config.DataFile("ordoor", "SMK/"+name+".smk")
if len(o.config.VideoPlayer) == 0 {
log.Printf("Video player not configured, skipping video %v", filename)

View File

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