Compare commits
16 Commits
132670507c
...
soldiers-a
Author | SHA1 | Date | |
---|---|---|---|
494fe4eb02 | |||
30d1786e64 | |||
65bae80d40 | |||
e8e9811b5d | |||
a6fdbaef2b | |||
0bf8233cd1 | |||
c2cbf1d95d | |||
54fe95239e | |||
63d3ee0ed6 | |||
5c869fc33c | |||
4358951e15 | |||
250a6033c8 | |||
f64af717b7 | |||
3866ee07a8 | |||
c1268e8d57 | |||
59baf20c35 |
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,14 +1,9 @@
|
||||
/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
|
||||
/WoW-CD
|
||||
/bin
|
||||
|
45
Makefile
45
Makefile
@@ -4,37 +4,40 @@ GOBUILD ?= go build -tags ebitengl
|
||||
|
||||
all: loader ordoor palette-idx view-ani view-font view-obj view-map view-menu view-minimap view-set
|
||||
|
||||
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
|
||||
|
176
README.md
176
README.md
@@ -1,23 +1,39 @@
|
||||
# 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
|
||||
|
||||
### Chaos Gate
|
||||
|
||||
Some of the original file formats are either partially or fully decoded. Maps,
|
||||
menus, and most visual data can be rendered pixel-perfect. Sound can be played
|
||||
(with a preprocessing step). Some UI tookit work is done. No game mechanics are
|
||||
@@ -31,10 +47,61 @@ 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.
|
||||
|
||||
### Soldiers At War
|
||||
|
||||
(At least some) objects display. Map support is being worked on in the
|
||||
`soldiers-at-war` branch, which can more-or-less display them, albeit with many
|
||||
errors.
|
||||
|
||||
### Squad Leader
|
||||
|
||||
Squad Leader is the most recent of the games created with this engine. Nothing
|
||||
has been done with it yet, but a preliminary look at the game data suggests many
|
||||
changes are afoot. The object files are a different format, at the very least.
|
||||
|
||||
### Wages of War
|
||||
|
||||
This is the oldest of the four games. The object file format seems to be mostly
|
||||
the same. the installer only copies some data to the game directory; we may want
|
||||
to work directly from the CDROM instead, if we can.
|
||||
|
||||
Maps are uncompressed, around 243K, and no header is present. They look similar
|
||||
in principle to the tile data of Soldiers At War or Chaos Gate maps, otherwise.
|
||||
|
||||
The menu system seen in Chaos Gate is not present; instead, there is a `BUTTONS`
|
||||
directory and a lot of `pcx` files under `PIC` that, I suspect, do the job for
|
||||
this game.
|
||||
|
||||
Even with a full installation, Wages of War leaves a lot of data on the CD. It
|
||||
may be best to run solely from the `WOW` directory on the CD, assuming it's a
|
||||
strict superset of what gets installed, data-wise.
|
||||
|
||||
## 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
|
||||
installed on your system:
|
||||
installed on your system. Dependency management uses `go mod`, so ensure you
|
||||
have at least Go 1.11.
|
||||
|
||||
```
|
||||
$ go version
|
||||
@@ -52,28 +119,25 @@ Debian:
|
||||
You can then run `make all` in the source tree to get the binaries that are
|
||||
present at the moment.
|
||||
|
||||
Place your WH40K: Chaos Gate installation in `./orig` to benefit from automatic
|
||||
path defaults. Otherwise, point to it with `-game-path`
|
||||
## Configuring
|
||||
|
||||
The `view-map` binary attempts to render a map, and is the current focus of
|
||||
effort. Once I can render a whole map, including pre-placed characters (cultist
|
||||
scum), things can start to get more interesting.
|
||||
Since we support multiple games, a fair bit of configuration is required. Copy
|
||||
`config.toml.example` to `config.toml` and edit it to your requirements. The
|
||||
`data_dir` for the engine(s) you want to use is probably the most important bit,
|
||||
along with the `default_engine`.
|
||||
|
||||
Current status: almost pixel-perfect map rendering. Static objects (four per map
|
||||
coordinate: floor, centre, left, and right) are rendered fine, and each Z level
|
||||
looks good. There are a few minor artifacts here and there.
|
||||
The various games all use snapshots of the original engine at different points
|
||||
in time, and specify a lot in code that we need to specify in data. That should
|
||||
all go into the config file, so new games will be able to adapt the engine to
|
||||
their needs.
|
||||
|
||||
Characters and animations aren't touched at all yet. Rendering performance is
|
||||
poor. No gameplay, no campaign logic. Interaction with the play area is minimal
|
||||
and limited to pan, zoom, and click for basic console output.
|
||||
|
||||
Still, I'm proud of myself.
|
||||
## Running
|
||||
|
||||
To run:
|
||||
|
||||
```
|
||||
$ make view-map
|
||||
$ ./view-map -map Chapter01
|
||||
$ ./bin/view-map -map Chapter01
|
||||
```
|
||||
|
||||
Looks like this:
|
||||
@@ -83,27 +147,29 @@ Looks like this:
|
||||
Use the arrow keys to scroll around the map, the mouse wheel to zoom, and the
|
||||
`1` - `7` keys to change Z level.
|
||||
|
||||
Dependency management uses `go mod`, so ensure you have at least Go 1.11.
|
||||
|
||||
There is the **start** of the menu / campaign flow in a `ordoor` binary:
|
||||
|
||||
```
|
||||
$ cp config.toml.example config.toml
|
||||
$ make ordoor
|
||||
$ ./ordoor
|
||||
```
|
||||
|
||||
This plays the introductory videos so far, and nothing else.
|
||||
|
||||
Menus are in the process of being rendered; you can use the `view-menu` binary
|
||||
to inspect them:
|
||||
Menus / UI widgets have fairly good support now; you can use the `view-menu`
|
||||
binary to inspect them:
|
||||
|
||||
```
|
||||
make view-menu
|
||||
./view-menu -menu ./orig/Menu/Main.mnu
|
||||
./bin/view-menu -menu Main
|
||||
```
|
||||
|
||||
This menu *displays* OK, including
|
||||
This renders the menus found in Chaos Gate and Soldiers At War. The Squad Leader
|
||||
format seems basically the same, but has some extra files and aren't 8-bit
|
||||
colour. They don't display at the moment. Wages of War uses a different format
|
||||
altogether.
|
||||
|
||||
For Chaos Gate, there is the **start** of the game in an `ordoor` binary:
|
||||
|
||||
```
|
||||
$ make ordoor
|
||||
$ ./bin/ordoor
|
||||
```
|
||||
|
||||
The idea is to hook all the different parts together, and to an abstract game
|
||||
state (which is called `ship` for ordoor), to make the whole thing playable. It
|
||||
isn't playable *yet*, but it's heading in that direction.
|
||||
|
||||
## Sound
|
||||
|
||||
@@ -118,31 +184,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!
|
||||
|
@@ -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)
|
||||
|
@@ -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,14 +46,19 @@ 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.Fatal("Failed to set up asset store: %v", err)
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to set up asset store: %v", err)
|
||||
}
|
||||
|
||||
state := state{
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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,14 +45,19 @@ 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.Fatal("Failed to set up asset store: %v", err)
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to set up asset store: %v", err)
|
||||
}
|
||||
|
||||
var obj *assetstore.Object
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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-CD"
|
||||
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.sl] # Squad Leader -> ??? -> ???
|
||||
data_dir = "./SL"
|
||||
palette = "ChaosGate" # may not be relevant?
|
||||
|
||||
[options]
|
||||
play_movies = true
|
||||
|
@@ -392,8 +392,11 @@ well-aligned amount.
|
||||
Investigation has so far suggested the following:
|
||||
|
||||
* `Cell[0]` seems related to doors and canisters. Observed:
|
||||
* Nothing special: 0x38
|
||||
* ???: 0x39
|
||||
* Imperial crate: 0x28
|
||||
* Door: 0xB8
|
||||
|
||||
* `Cell[1]` seems related to special placeables (but not triggers). Bitfield. Observed:
|
||||
* 0x01: Reactor
|
||||
* 0x20: Door or door lock?
|
||||
@@ -408,12 +411,12 @@ Investigation has so far suggested the following:
|
||||
* `Cell[7]` Object 2 (Right) Area (Sets/*.set lookup)
|
||||
* `Cell[6]` Object 2 (Right) Sprite + active flag
|
||||
* `Cell[9]` Object 3 (Center) Area (Sets/*.set lookup)
|
||||
* `Cell[10]` Object 3 (Right) Sprite + active flag
|
||||
* `Cell[11]` all 255?
|
||||
* `Cell[10]` Object 3 (Center) Sprite + active flag
|
||||
* `Cell[11]` all 255? Vehicle?
|
||||
* `Cell[12]` all 0?
|
||||
* `Cell[13]` all 0?
|
||||
* `Cell[14]` all 0?
|
||||
* `Cell[15]` shows squad positions, MP start positions, etc, as 0x04
|
||||
* `Cell[15]` shows squad positions, MP start positions, etc, as 0x04. Bitfield?
|
||||
|
||||
Mapping the altar in Chapter01 to the map01 set suggests it's a palette entry
|
||||
lookup, 0-indexed. `U` debug in WH40K_TD.exe says the cell's `Object 3-Center`
|
||||
@@ -515,10 +518,120 @@ Around 001841A0: mission objectives!
|
||||
00184240 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||
```
|
||||
|
||||
Since all the files are exactly the same length uncompressed, I'm going to
|
||||
assume these are all a fixed number of fixed-size records when looking into it.
|
||||
Relative offsets from the start of the trailer, we have:
|
||||
|
||||
| Offset | Text |
|
||||
| -------- | ---- |
|
||||
| `0xEE` | Mania |
|
||||
| `0x78A` | Dagon |
|
||||
| `0xE26` | Nihasa |
|
||||
| `0x14C2` | Samnu |
|
||||
| `0x1b5e` | Bael |
|
||||
| `0x2896` | Gigamen |
|
||||
| `0x2f32` | Valefor |
|
||||
| `0x35ce` | Baalberith |
|
||||
| `0x3c6a` | Fenriz |
|
||||
| `0x4306` | #Character |
|
||||
| `0x49a2` | Apollyon |
|
||||
|
||||
So there are 1692 bytes between each name (the names probably don't come at the
|
||||
start of each block, but it's still a useful stride). Presumably `#Character` is
|
||||
a space for one of the player characters, while the others specify an NPC placed
|
||||
on the map.
|
||||
|
||||
There's 56 of these records between the first and last name we see - `Ahpuch`.
|
||||
|
||||
Then there are a number of other strings that seem related to triggers / events,
|
||||
including lots that say `NO FILE`. The first two are 96 bytes apart; from then
|
||||
on they seem to be placed variably apart from each other; I've seen 96, 256, and
|
||||
352 byte offsets.
|
||||
|
||||
At 0x20916 the mission objective is readable.
|
||||
|
||||
At 0x2092a the mission description is readable.
|
||||
|
||||
Generating another map with just 5 characters on it, things look different:
|
||||
|
||||
* Trailer size is 13543 bytes
|
||||
* There are only 5 names
|
||||
* There are none of the trigger/event strings
|
||||
* Mission title is found at 0x2b93
|
||||
* Mission briefing is found at 0x2c92
|
||||
|
||||
Since the trailer is a variable size, there must be a header that tells us how
|
||||
many of each type of record to read. Peeking at the differences in `vbindiff`:
|
||||
|
||||
```
|
||||
Chapter01.MAP.Trailer
|
||||
0000 0000: 38 00 00 68 00 00 00 50 00 00 00 1A 00 00 00 14 8..h...P ........
|
||||
0000 0010: 00 00 00 3A 00 00 00 00 38 25 00 04 00 00 00 00 ...:.... 8%......
|
||||
0000 0020: 00 00 00 1A 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
|
||||
0000 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
|
||||
0000 0040: 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 ........ ........
|
||||
0000 0050: 00 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 ...2.... ........
|
||||
|
||||
TINYSQUAD.MAP.Trailer
|
||||
0000 0000: 38 00 00 4B 00 00 00 3C 00 00 00 37 00 00 00 28 8..K...< ...7...(
|
||||
0000 0010: 00 00 00 05 00 00 00 00 2B 3A 00 04 00 00 00 05 ........ +:......
|
||||
0000 0020: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
|
||||
0000 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
|
||||
0000 0040: 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 ........ ........
|
||||
0000 0050: 00 00 00 32 00 00 00 00 00 00 00 00 00 00 00 00 ...2.... ........
|
||||
```
|
||||
|
||||
The size of the trailer for Chapter01 is 139,483 bytes, assuming it starts at
|
||||
`0x163890`. However, things may be a lot more sensible if we drop 3 bytes off
|
||||
the start of that to get the fields into little-endian alignment. Have I made a
|
||||
maths error above somewhere? Is it some sort of alignment thing? Do those 3
|
||||
bytes actually have meaning?
|
||||
|
||||
Ignoring them for now, here's a first guess at a header:
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ------ | ---- | ------- |
|
||||
| 0 | 4 | Map maximum X + 1 |
|
||||
| 4 | 4 | Map maximum Y + 1 |
|
||||
| 8 | 4 | Map minimum X |
|
||||
| 12 | 4 | Map minimum Y |
|
||||
| 16 | 4 | Number of character records |
|
||||
| 20 | 4 | Padding? - invariant `00 00 00 00` |
|
||||
| 24 | 2 | ??? - varies. Seems related to character/squad position? |
|
||||
| 26 | 2 | ??? - invariant `00 04` |
|
||||
| 28 | 4 | ??? - varies (0 vs 5) |
|
||||
| 32 | 4 | Number of thingies (26 vs 1) |
|
||||
| 36 | 20 | Padding? |
|
||||
|
||||
56 bytes of data is interesting because the value of that first, ignored byte is
|
||||
0x38 - perhaps it's a skip value + 2 bytes of padding? It's just weird. Keep
|
||||
ignoring it for now.
|
||||
|
||||
0x4b contains the next non-null byte; is the gap between the the number of
|
||||
thingies, and it, padding? Minus a bit? 0x50 is another non-null byte. Then
|
||||
it's all zeroes until one byte before the first name at 0xee.
|
||||
|
||||
It's hard to say where the alignment should be at this point. We need to compare
|
||||
more characters with each other. Other notes...
|
||||
|
||||
Characters are organised into Squads somehow.
|
||||
|
||||
Individual cells seem to have a flag to say "We have a character in us", but not
|
||||
the number for the character themselves, so the coordinates must be in the
|
||||
per-character records also. There are several candidates for this.
|
||||
|
||||
Placing a single character at (64,49) causes those bytes to show up at four
|
||||
offsets - 0x18 (!), 0x1F4, 0x1F8, and 0x6C8.
|
||||
|
||||
Generating a map with no characters at all, the trailer is 2,447 bytes, and the
|
||||
mission title starts at 0x3B (59). So we can say we have 20 bytes of padding as
|
||||
a first approximation?
|
||||
|
||||
The "trailer trailer", for want of a better term, seems to be organised as:
|
||||
|
||||
| Offset | Size | Meaning |
|
||||
| ----- | ---- | ------- |
|
||||
| 0 | 255 | Title |
|
||||
| 255 | 2048 | Briefing |
|
||||
| 2304 | 85 | ??? - each byte is 1 or 0. Spaced so it may be partly uint32 |
|
||||
|
||||
|
||||
## Soldiers At War
|
||||
@@ -600,3 +713,15 @@ xxd -s 0xc0 -c 13 -l 260 -g 13 BIGGESTMAP.MAP
|
||||
000000e7: 00 00 85 01 00 00 00 00 00 ff 00 00 1f .............
|
||||
# ...
|
||||
```
|
||||
|
||||
This can be interpreted more or less the same way as the Chaos Gate maps now,
|
||||
and the `soldiers-at-war` branch contains a hacked-up implementation that kind
|
||||
of works \o/.
|
||||
|
||||
Does the same trailer apply? Seemingly not. Looking at `PARIS.MAP`, there's no
|
||||
similarity at first glance.
|
||||
|
||||
However, I did manage to track down 4 32-bit ints inside the trailer, starting
|
||||
at `0x121ad1`, which specify dimensions of the map, at least. Perhaps the
|
||||
position has moved, but some of the data is the same? It's 3320 bytes into the
|
||||
trailer.
|
||||
|
1
go.mod
1
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4
|
||||
github.com/stretchr/testify v1.5.1
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1
|
||||
|
2
go.sum
2
go.sum
@@ -45,6 +45,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4 h1:Y/KOCu+ZLB730PudefxfsKVjtI0m0RhvFk9a0l4O1+c=
|
||||
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4/go.mod h1:qxuIawynlRhuaHowuXvd1xjyFWx87Ro4gkZlKRXtHnQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
|
@@ -2,24 +2,25 @@ package assetstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
|
||||
"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 +28,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" }, ...}
|
||||
@@ -41,6 +46,7 @@ type AssetStore struct {
|
||||
fonts map[string]*Font
|
||||
generic *data.Generic
|
||||
idx *idx.Idx
|
||||
images map[string]*ebiten.Image
|
||||
maps map[string]*Map
|
||||
menus map[string]*Menu
|
||||
objs map[string]*Object
|
||||
@@ -50,9 +56,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 +86,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)
|
||||
@@ -96,6 +112,7 @@ func (a *AssetStore) Refresh() error {
|
||||
a.entries = newEntryMap
|
||||
a.fonts = make(map[string]*Font)
|
||||
a.idx = nil
|
||||
a.images = make(map[string]*ebiten.Image)
|
||||
a.maps = make(map[string]*Map)
|
||||
a.menus = make(map[string]*Menu)
|
||||
a.objs = make(map[string]*Object)
|
||||
@@ -118,7 +135,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
|
||||
}
|
||||
}
|
||||
|
42
internal/assetstore/image.go
Normal file
42
internal/assetstore/image.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package assetstore
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
_ "github.com/samuel/go-pcx/pcx" // PCX support
|
||||
)
|
||||
|
||||
func (a *AssetStore) Image(name string) (*ebiten.Image, error) {
|
||||
name = canonical(name)
|
||||
if img, ok := a.images[name]; ok {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// baps, ordoor, geas store .pcx files in Pic
|
||||
// TODO: SL stores .bmp files in Res
|
||||
filename, err := a.lookup(name, "pcx", "Pic")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
rawImg, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img, err := ebiten.NewImageFromImage(rawImg, ebiten.FilterDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.images[name] = img
|
||||
return img, nil
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -42,7 +42,7 @@ func (a *AssetStore) Object(name string) (*Object, error) {
|
||||
}
|
||||
log.Printf("Loading object %v", name)
|
||||
|
||||
filename, err := a.lookup(name, "obj", "Obj")
|
||||
filename, err := a.lookup(name, "obj", "Obj", "spr")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
@@ -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")
|
||||
|
@@ -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 {
|
||||
@@ -28,7 +28,11 @@ type SpriteHeader struct {
|
||||
|
||||
func (s SpriteHeader) Check(expectedSize uint32) error {
|
||||
if s.Padding1 != 0 || s.Padding2 != 0 {
|
||||
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
|
||||
if s.Padding1 == 271 && s.Padding2 == 0 {
|
||||
log.Printf("Sprite header padding matches FIXME value")
|
||||
} else {
|
||||
return fmt.Errorf("Sprite header padding contains unknown values: %d %d", s.Padding1, s.Padding2)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: WarHammer.ani sets Unknown1 to this for all 188,286 sprites. I am
|
||||
@@ -53,12 +57,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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
expectedMagic = []byte("\x08\x00WHMAP\x00")
|
||||
expectedSetNameOffset = uint32(0x34)
|
||||
expectedMagic = []byte("\x15\x00AMB_MAP\x00")
|
||||
expectedSetNameOffset = uint32(0x10)
|
||||
notImplemented = fmt.Errorf("Not implemented")
|
||||
)
|
||||
|
||||
@@ -25,41 +25,53 @@ const (
|
||||
MaxLength = 100 // Y coordinate
|
||||
MaxWidth = 130 // X coordinate
|
||||
|
||||
CellSize = 16 // seems to be
|
||||
CellSize = 13 // seems to be
|
||||
|
||||
cellDataOffset = 0x110 // definitely
|
||||
cellDataOffset = 0xc0
|
||||
cellCount = MaxHeight * MaxLength * MaxWidth
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes
|
||||
MinWidth uint32
|
||||
MinLength uint32
|
||||
MaxWidth uint32
|
||||
MaxLength uint32
|
||||
Unknown1 uint32
|
||||
Unknown2 uint32
|
||||
Unknown3 uint32
|
||||
Unknown4 uint32
|
||||
Magic [8]byte // "\x08\x00WHMAP\x00"
|
||||
Unknown5 uint32
|
||||
Unknown6 uint32
|
||||
SetName [8]byte // Links to a filename in `/Sets/*.set`
|
||||
Magic [10]byte // "\x15\x00AMB_MAP\x00"
|
||||
SetName [8]byte // Links to a filename in `/Sets/*.set`
|
||||
// Need to investigate the rest of the header too
|
||||
IsCampaignMap byte
|
||||
}
|
||||
|
||||
func (h Header) Width() int {
|
||||
return int(h.MaxWidth - h.MinWidth)
|
||||
type TrailerHeader struct {
|
||||
Discard1 [3]byte // No idea what this lot is
|
||||
MaxWidth uint32
|
||||
MaxLength uint32
|
||||
MinWidth uint32
|
||||
MinLength uint32
|
||||
|
||||
NumCharacters uint32
|
||||
|
||||
Unknown1 uint32
|
||||
Unknown2 uint16
|
||||
Unknown3 uint16
|
||||
Unknown4 uint32
|
||||
NumThingies uint32
|
||||
Padding1 [20]byte
|
||||
}
|
||||
|
||||
func (h Header) Length() int {
|
||||
return int(h.MaxLength - h.MinLength)
|
||||
type TrailerTrailer struct {
|
||||
Title [255]byte
|
||||
Briefing [2048]byte
|
||||
Unknown1 [85]uint8 // Maybe? each contains either 0 or 1? Hard to say
|
||||
}
|
||||
|
||||
func (h Header) Height() int {
|
||||
return MaxHeight
|
||||
type Character struct {
|
||||
Unknown1 uint32
|
||||
}
|
||||
|
||||
type Characters []Character
|
||||
|
||||
// TODO. These are triggers/reactors/etc.
|
||||
type Thingy struct {}
|
||||
|
||||
type Thingies []Thingy
|
||||
|
||||
func (h Header) MapSetName() string {
|
||||
idx := bytes.IndexByte(h.SetName[:], 0)
|
||||
if idx < 0 {
|
||||
@@ -80,7 +92,7 @@ type ObjRef struct {
|
||||
|
||||
// The index into a set palette to retrieve the object
|
||||
func (o ObjRef) Index() int {
|
||||
return int(o.AreaByte)
|
||||
return int(o.AreaByte & 0x7f)
|
||||
}
|
||||
|
||||
func (o ObjRef) Sprite() int {
|
||||
@@ -91,12 +103,13 @@ func (o ObjRef) Sprite() int {
|
||||
// The top bit seems to say whether we should draw or not.
|
||||
func (o ObjRef) IsActive() bool {
|
||||
return (o.SpriteAndFlagByte & 0x80) == 0x80
|
||||
}
|
||||
|
||||
} // PARIS is 78 x 60 x 7
|
||||
// 4E 3C 7
|
||||
/*
|
||||
type Cell struct {
|
||||
DoorAndCanisterRelated byte
|
||||
DoorLockAndReactorRelated byte
|
||||
Unknown2 byte
|
||||
// DoorLockAndReactorRelated byte
|
||||
// Unknown2 byte
|
||||
Surface ObjRef
|
||||
Left ObjRef
|
||||
Right ObjRef
|
||||
@@ -105,43 +118,60 @@ type Cell struct {
|
||||
Unknown12 byte
|
||||
Unknown13 byte
|
||||
Unknown14 byte
|
||||
SquadRelated byte
|
||||
// SquadRelated byte
|
||||
}*/
|
||||
|
||||
type Cell struct {
|
||||
Unknown1 byte
|
||||
Surface ObjRef
|
||||
Left ObjRef
|
||||
Right ObjRef
|
||||
Center ObjRef
|
||||
Unknown2 [4]byte
|
||||
|
||||
/*
|
||||
DoorAndCanisterRelated byte
|
||||
// DoorLockAndReactorRelated byte
|
||||
// Unknown2 byte
|
||||
Surface ObjRef
|
||||
Left ObjRef
|
||||
Right ObjRef
|
||||
Center ObjRef
|
||||
Unknown11 byte
|
||||
Unknown12 byte
|
||||
Unknown13 byte
|
||||
Unknown14 byte
|
||||
SquadRelated byte*/
|
||||
}
|
||||
|
||||
func (c *Cell) At(n int) byte {
|
||||
switch n {
|
||||
case 0:
|
||||
return c.DoorAndCanisterRelated
|
||||
return c.Unknown1
|
||||
case 1:
|
||||
return c.DoorLockAndReactorRelated
|
||||
case 2:
|
||||
return c.Unknown2
|
||||
case 3:
|
||||
return c.Surface.AreaByte
|
||||
case 4:
|
||||
case 2:
|
||||
return c.Surface.SpriteAndFlagByte
|
||||
case 5:
|
||||
case 3:
|
||||
return c.Left.AreaByte
|
||||
case 6:
|
||||
case 4:
|
||||
return c.Left.SpriteAndFlagByte
|
||||
case 7:
|
||||
case 5:
|
||||
return c.Right.AreaByte
|
||||
case 8:
|
||||
case 6:
|
||||
return c.Right.SpriteAndFlagByte
|
||||
case 9:
|
||||
case 7:
|
||||
return c.Center.AreaByte
|
||||
case 10:
|
||||
case 8:
|
||||
return c.Center.SpriteAndFlagByte
|
||||
case 9:
|
||||
return c.Unknown2[0]
|
||||
case 10:
|
||||
return c.Unknown2[1]
|
||||
case 11:
|
||||
return c.Unknown11
|
||||
return c.Unknown2[2]
|
||||
case 12:
|
||||
return c.Unknown12
|
||||
case 13:
|
||||
return c.Unknown13
|
||||
case 14:
|
||||
return c.Unknown14
|
||||
case 15:
|
||||
return c.SquadRelated
|
||||
return c.Unknown2[3]
|
||||
}
|
||||
|
||||
return 0
|
||||
@@ -150,15 +180,23 @@ func (c *Cell) At(n int) byte {
|
||||
// Cells is always a fixed size; use At to get a cell according to x,y,z
|
||||
type Cells []Cell
|
||||
|
||||
// 6 Possibilities for being laid out in memory. Most likely:
|
||||
// XXYYZZ
|
||||
// OR
|
||||
// XYZXYZ
|
||||
|
||||
func (c Cells) At(x, y, z int) Cell {
|
||||
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
|
||||
// log.Printf("At (%v,%v,%v)=%v", x, y, z, x*y*z)
|
||||
return c[(z*MaxLength*MaxWidth)+
|
||||
(y*MaxWidth)+
|
||||
x]
|
||||
}
|
||||
|
||||
func (h Header) Check() []error {
|
||||
var out []error
|
||||
if h.IsCampaignMap > 1 {
|
||||
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
|
||||
}
|
||||
// if h.IsCampaignMap > 1 {
|
||||
// out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
|
||||
// }
|
||||
|
||||
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 {
|
||||
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
|
||||
@@ -170,16 +208,23 @@ func (h Header) Check() []error {
|
||||
type GameMap struct {
|
||||
Header
|
||||
Cells
|
||||
// TODO: parse this into sections
|
||||
TrailerHeader
|
||||
Characters
|
||||
Thingies
|
||||
TrailerTrailer
|
||||
|
||||
// TODO: parse this into sections. This is the text that comes from the
|
||||
// .TXT file. Maybe we don't need it at all since it should be duplicated in
|
||||
// the trailer.
|
||||
Text string
|
||||
}
|
||||
|
||||
func (m *GameMap) Rect() image.Rectangle {
|
||||
return image.Rect(
|
||||
int(m.Header.MinWidth),
|
||||
int(m.Header.MinLength),
|
||||
int(m.Header.MaxWidth),
|
||||
int(m.Header.MaxLength),
|
||||
int(m.TrailerHeader.MinWidth),
|
||||
int(m.TrailerHeader.MinLength),
|
||||
int(m.TrailerHeader.MaxWidth-1),
|
||||
int(m.TrailerHeader.MaxLength-1),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -286,5 +331,42 @@ func loadMapFile(filename string) (*GameMap, error) {
|
||||
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err)
|
||||
}
|
||||
|
||||
// no gzip.SeekReader, so discard unread trailer bytes for now
|
||||
if _, err := io.CopyN(ioutil.Discard, zr, int64(3320-2)); err != nil { // observed
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerHeader); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing trailer header for %s: %v", filename, err)
|
||||
}
|
||||
|
||||
log.Printf("Trailer Header: %#+v", out.TrailerHeader)
|
||||
/*
|
||||
// TODO: until we know how large each character record should be, we can't read this lot
|
||||
|
||||
out.Characters = make(Characters, int(out.TrailerHeader.NumCharacters))
|
||||
if err := binary.Read(zr, binary.LittleEndian, &out.Characters); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing characters for %s: %v", filename, err)
|
||||
}
|
||||
|
||||
out.Thingies = make(Thingies, int(out.TrailerHeader.NumThingies))
|
||||
if err := binary.Read(zr, binary.LittleEndian, &out.Thingies); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing thingies for %s: %v", filename, err)
|
||||
}
|
||||
|
||||
if err := binary.Read(zr, binary.LittleEndian, &out.TrailerTrailer); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing trailer trailer for %s: %v", filename, err)
|
||||
}
|
||||
log.Printf("Trailer Trailer: %s", out.TrailerTrailer.String())
|
||||
*/
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (t *TrailerTrailer) String() string {
|
||||
return fmt.Sprintf(
|
||||
"title=%q briefing=%q rest=%#+v",
|
||||
strings.TrimRight(string(t.Title[:]), "\x00"),
|
||||
strings.TrimRight(string(t.Briefing[:]), "\x00"),
|
||||
t.Unknown1,
|
||||
)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
20
internal/ordoor/display.go
Normal file
20
internal/ordoor/display.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package ordoor
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (o *Ordoor) DisplayImageFor(d time.Duration, name string) error {
|
||||
img, err := o.assets.Image(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.pic = img
|
||||
go func() {
|
||||
<-time.After(d)
|
||||
o.pic = nil // FIXME: this is a race condition and a half
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
@@ -7,6 +7,7 @@ package ordoor
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/audio"
|
||||
@@ -29,17 +30,21 @@ type Ordoor struct {
|
||||
// Relevant to interface state
|
||||
flow *flow.Flow
|
||||
|
||||
// FIXME: should be put inside flow
|
||||
// If this is set, we display it instead of flow
|
||||
pic *ebiten.Image
|
||||
|
||||
// Relevant to campaign state
|
||||
ship *ship.Ship
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -81,10 +86,6 @@ func Run(configFile string, overrideX, overrideY int) error {
|
||||
|
||||
ordoor.win = win
|
||||
|
||||
if err := ordoor.setupFlow(); err != nil {
|
||||
return fmt.Errorf("failed to setup UI flow: %v", err)
|
||||
}
|
||||
|
||||
if err := ordoor.Run(); err != nil {
|
||||
return fmt.Errorf("Run finished with error: %v", err)
|
||||
}
|
||||
@@ -93,12 +94,16 @@ func Run(configFile string, overrideX, overrideY int) error {
|
||||
}
|
||||
|
||||
func (o *Ordoor) Run() error {
|
||||
// FIXME: we're missing a screen about SSI here
|
||||
// FIXME: these should be displayed *after*, not *before*, the copyright
|
||||
if o.config.Options.PlayMovies {
|
||||
o.PlaySkippableVideo("LOGOS")
|
||||
o.PlaySkippableVideo("movie1")
|
||||
}
|
||||
|
||||
if err := o.DisplayImageFor(time.Second, "copyright"); err != nil {
|
||||
log.Printf("Failed to display copyright image: %v", err)
|
||||
}
|
||||
|
||||
err := o.win.Run()
|
||||
if err == flow.ErrExit {
|
||||
log.Printf("Exit requested")
|
||||
@@ -151,6 +156,16 @@ func (o *Ordoor) setupFlow() error {
|
||||
}
|
||||
|
||||
func (o *Ordoor) Update(screenX, screenY int) error {
|
||||
if pic := o.pic; pic != nil {
|
||||
return nil // Ignore flow until we don't have a pic any more
|
||||
}
|
||||
|
||||
if o.flow == nil {
|
||||
if err := o.setupFlow(); err != nil {
|
||||
return fmt.Errorf("failed to setup UI flow: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure music is doing the right thing
|
||||
if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic {
|
||||
if o.config.Options.PlayMusic {
|
||||
@@ -165,9 +180,24 @@ func (o *Ordoor) Update(screenX, screenY int) error {
|
||||
}
|
||||
|
||||
func (o *Ordoor) Draw(screen *ebiten.Image) error {
|
||||
if pic := o.pic; pic != nil {
|
||||
// Scale the picture to the screen and draw it
|
||||
scaleX := float64(screen.Bounds().Dx()) / float64(pic.Bounds().Dx())
|
||||
scaleY := float64(screen.Bounds().Dy()) / float64(pic.Bounds().Dy())
|
||||
|
||||
do := &ebiten.DrawImageOptions{}
|
||||
do.GeoM.Scale(scaleX, scaleY)
|
||||
|
||||
return screen.DrawImage(pic, do)
|
||||
}
|
||||
|
||||
return o.flow.Draw(screen)
|
||||
}
|
||||
|
||||
func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
||||
return o.flow.Cursor()
|
||||
if o.flow != nil {
|
||||
return o.flow.Cursor()
|
||||
}
|
||||
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
269
internal/palettes/wages_of_war.go
Normal file
269
internal/palettes/wages_of_war.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package palettes
|
||||
|
||||
import "image/color"
|
||||
|
||||
var (
|
||||
WagesOfWarPalette = color.Palette{
|
||||
Transparent,
|
||||
|
||||
color.RGBA{R: 1, G: 1, B: 1, A: 255},
|
||||
color.RGBA{R: 254, G: 254, B: 254, A: 255},
|
||||
color.RGBA{R: 187, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 181, G: 195, B: 2, A: 255},
|
||||
color.RGBA{R: 94, G: 79, B: 148, A: 255},
|
||||
color.RGBA{R: 233, G: 91, B: 4, A: 255},
|
||||
color.RGBA{R: 60, G: 60, B: 60, A: 255},
|
||||
color.RGBA{R: 102, G: 102, B: 102, A: 255},
|
||||
color.RGBA{R: 170, G: 170, B: 170, A: 255},
|
||||
color.RGBA{R: 197, G: 195, B: 193, A: 255},
|
||||
color.RGBA{R: 234, G: 234, B: 235, A: 255},
|
||||
color.RGBA{R: 218, G: 218, B: 240, A: 255},
|
||||
color.RGBA{R: 202, G: 202, B: 245, A: 255},
|
||||
color.RGBA{R: 186, G: 186, B: 250, A: 255},
|
||||
color.RGBA{R: 170, G: 170, B: 255, A: 255},
|
||||
color.RGBA{R: 238, G: 238, B: 238, A: 255},
|
||||
color.RGBA{R: 222, G: 222, B: 222, A: 255},
|
||||
color.RGBA{R: 206, G: 206, B: 206, A: 255},
|
||||
color.RGBA{R: 190, G: 190, B: 190, A: 255},
|
||||
color.RGBA{R: 174, G: 174, B: 174, A: 255},
|
||||
color.RGBA{R: 157, G: 157, B: 157, A: 255},
|
||||
color.RGBA{R: 141, G: 141, B: 141, A: 255},
|
||||
color.RGBA{R: 129, G: 129, B: 129, A: 255},
|
||||
color.RGBA{R: 113, G: 113, B: 113, A: 255},
|
||||
color.RGBA{R: 97, G: 97, B: 97, A: 255},
|
||||
color.RGBA{R: 80, G: 80, B: 80, A: 255},
|
||||
color.RGBA{R: 64, G: 64, B: 64, A: 255},
|
||||
color.RGBA{R: 48, G: 48, B: 48, A: 255},
|
||||
color.RGBA{R: 32, G: 32, B: 32, A: 255},
|
||||
color.RGBA{R: 16, G: 16, B: 16, A: 255},
|
||||
color.RGBA{R: 4, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 255, G: 238, B: 238, A: 255},
|
||||
color.RGBA{R: 246, G: 194, B: 194, A: 255},
|
||||
color.RGBA{R: 238, G: 153, B: 153, A: 255},
|
||||
color.RGBA{R: 230, G: 117, B: 117, A: 255},
|
||||
color.RGBA{R: 222, G: 80, B: 80, A: 255},
|
||||
color.RGBA{R: 214, G: 48, B: 48, A: 255},
|
||||
color.RGBA{R: 210, G: 16, B: 16, A: 255},
|
||||
color.RGBA{R: 190, G: 12, B: 12, A: 255},
|
||||
color.RGBA{R: 170, G: 8, B: 8, A: 255},
|
||||
color.RGBA{R: 153, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 133, G: 4, B: 4, A: 255},
|
||||
color.RGBA{R: 113, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 97, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 76, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 56, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 40, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 250, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 165, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 80, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 0, A: 255},
|
||||
color.RGBA{R: 234, G: 222, B: 12, A: 255},
|
||||
color.RGBA{R: 214, G: 194, B: 28, A: 255},
|
||||
color.RGBA{R: 194, G: 170, B: 40, A: 255},
|
||||
color.RGBA{R: 174, G: 149, B: 52, A: 255},
|
||||
color.RGBA{R: 255, G: 202, B: 153, A: 255},
|
||||
color.RGBA{R: 255, G: 170, B: 101, A: 255},
|
||||
color.RGBA{R: 255, G: 133, B: 48, A: 255},
|
||||
color.RGBA{R: 255, G: 93, B: 0, A: 255},
|
||||
color.RGBA{R: 222, G: 89, B: 12, A: 255},
|
||||
color.RGBA{R: 190, G: 85, B: 28, A: 255},
|
||||
color.RGBA{R: 157, G: 76, B: 36, A: 255},
|
||||
color.RGBA{R: 129, G: 68, B: 40, A: 255},
|
||||
color.RGBA{R: 198, G: 255, B: 206, A: 255},
|
||||
color.RGBA{R: 170, G: 238, B: 182, A: 255},
|
||||
color.RGBA{R: 149, G: 222, B: 157, A: 255},
|
||||
color.RGBA{R: 125, G: 206, B: 137, A: 255},
|
||||
color.RGBA{R: 105, G: 190, B: 113, A: 255},
|
||||
color.RGBA{R: 89, G: 174, B: 97, A: 255},
|
||||
color.RGBA{R: 72, G: 157, B: 76, A: 255},
|
||||
color.RGBA{R: 56, G: 141, B: 64, A: 255},
|
||||
color.RGBA{R: 40, G: 125, B: 48, A: 255},
|
||||
color.RGBA{R: 32, G: 109, B: 36, A: 255},
|
||||
color.RGBA{R: 20, G: 93, B: 24, A: 255},
|
||||
color.RGBA{R: 12, G: 76, B: 16, A: 255},
|
||||
color.RGBA{R: 4, G: 60, B: 8, A: 255},
|
||||
color.RGBA{R: 0, G: 44, B: 4, A: 255},
|
||||
color.RGBA{R: 0, G: 32, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 16, B: 0, A: 255},
|
||||
color.RGBA{R: 210, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 182, G: 238, B: 234, A: 255},
|
||||
color.RGBA{R: 157, G: 222, B: 218, A: 255},
|
||||
color.RGBA{R: 137, G: 210, B: 202, A: 255},
|
||||
color.RGBA{R: 113, G: 194, B: 186, A: 255},
|
||||
color.RGBA{R: 97, G: 178, B: 165, A: 255},
|
||||
color.RGBA{R: 76, G: 165, B: 149, A: 255},
|
||||
color.RGBA{R: 64, G: 149, B: 129, A: 255},
|
||||
color.RGBA{R: 48, G: 133, B: 113, A: 255},
|
||||
color.RGBA{R: 36, G: 121, B: 97, A: 255},
|
||||
color.RGBA{R: 24, G: 105, B: 80, A: 255},
|
||||
color.RGBA{R: 16, G: 89, B: 64, A: 255},
|
||||
color.RGBA{R: 8, G: 76, B: 52, A: 255},
|
||||
color.RGBA{R: 4, G: 60, B: 36, A: 255},
|
||||
color.RGBA{R: 0, G: 44, B: 24, A: 255},
|
||||
color.RGBA{R: 0, G: 32, B: 16, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 170, A: 255},
|
||||
color.RGBA{R: 238, G: 238, B: 149, A: 255},
|
||||
color.RGBA{R: 222, G: 222, B: 129, A: 255},
|
||||
color.RGBA{R: 206, G: 210, B: 109, A: 255},
|
||||
color.RGBA{R: 190, G: 194, B: 93, A: 255},
|
||||
color.RGBA{R: 174, G: 182, B: 76, A: 255},
|
||||
color.RGBA{R: 157, G: 165, B: 64, A: 255},
|
||||
color.RGBA{R: 141, G: 149, B: 52, A: 255},
|
||||
color.RGBA{R: 125, G: 137, B: 40, A: 255},
|
||||
color.RGBA{R: 109, G: 121, B: 28, A: 255},
|
||||
color.RGBA{R: 97, G: 109, B: 20, A: 255},
|
||||
color.RGBA{R: 80, G: 93, B: 12, A: 255},
|
||||
color.RGBA{R: 64, G: 76, B: 4, A: 255},
|
||||
color.RGBA{R: 52, G: 64, B: 4, A: 255},
|
||||
color.RGBA{R: 40, G: 48, B: 0, A: 255},
|
||||
color.RGBA{R: 28, G: 36, B: 0, A: 255},
|
||||
color.RGBA{R: 214, G: 255, B: 214, A: 255},
|
||||
color.RGBA{R: 157, G: 255, B: 157, A: 255},
|
||||
color.RGBA{R: 105, G: 255, B: 105, A: 255},
|
||||
color.RGBA{R: 48, G: 255, B: 48, A: 255},
|
||||
color.RGBA{R: 0, G: 255, B: 0, A: 255},
|
||||
color.RGBA{R: 24, G: 210, B: 28, A: 255},
|
||||
color.RGBA{R: 44, G: 165, B: 48, A: 255},
|
||||
color.RGBA{R: 52, G: 125, B: 56, A: 255},
|
||||
color.RGBA{R: 226, G: 121, B: 0, A: 255},
|
||||
color.RGBA{R: 198, G: 89, B: 0, A: 255},
|
||||
color.RGBA{R: 174, G: 64, B: 0, A: 255},
|
||||
color.RGBA{R: 145, G: 40, B: 0, A: 255},
|
||||
color.RGBA{R: 121, G: 24, B: 0, A: 255},
|
||||
color.RGBA{R: 93, G: 8, B: 0, A: 255},
|
||||
color.RGBA{R: 64, G: 4, B: 0, A: 255},
|
||||
color.RGBA{R: 40, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 194, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 165, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 109, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 0, G: 255, B: 255, A: 255},
|
||||
color.RGBA{R: 4, G: 222, B: 222, A: 255},
|
||||
color.RGBA{R: 16, G: 194, B: 194, A: 255},
|
||||
color.RGBA{R: 20, G: 161, B: 161, A: 255},
|
||||
color.RGBA{R: 24, G: 133, B: 133, A: 255},
|
||||
color.RGBA{R: 137, G: 157, B: 255, A: 255},
|
||||
color.RGBA{R: 101, G: 121, B: 255, A: 255},
|
||||
color.RGBA{R: 64, G: 80, B: 255, A: 255},
|
||||
color.RGBA{R: 28, G: 40, B: 255, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 255, A: 255},
|
||||
color.RGBA{R: 12, G: 12, B: 206, A: 255},
|
||||
color.RGBA{R: 20, G: 20, B: 157, A: 255},
|
||||
color.RGBA{R: 24, G: 24, B: 109, A: 255},
|
||||
color.RGBA{R: 234, G: 234, B: 255, A: 255},
|
||||
color.RGBA{R: 202, G: 202, B: 238, A: 255},
|
||||
color.RGBA{R: 178, G: 178, B: 222, A: 255},
|
||||
color.RGBA{R: 153, G: 153, B: 206, A: 255},
|
||||
color.RGBA{R: 129, G: 129, B: 194, A: 255},
|
||||
color.RGBA{R: 105, G: 105, B: 178, A: 255},
|
||||
color.RGBA{R: 89, G: 89, B: 161, A: 255},
|
||||
color.RGBA{R: 68, G: 68, B: 145, A: 255},
|
||||
color.RGBA{R: 52, G: 52, B: 133, A: 255},
|
||||
color.RGBA{R: 40, G: 40, B: 117, A: 255},
|
||||
color.RGBA{R: 28, G: 28, B: 101, A: 255},
|
||||
color.RGBA{R: 16, G: 16, B: 89, A: 255},
|
||||
color.RGBA{R: 8, G: 8, B: 72, A: 255},
|
||||
color.RGBA{R: 4, G: 4, B: 56, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 40, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 28, A: 255},
|
||||
color.RGBA{R: 250, G: 218, B: 170, A: 255},
|
||||
color.RGBA{R: 234, G: 198, B: 145, A: 255},
|
||||
color.RGBA{R: 218, G: 182, B: 129, A: 255},
|
||||
color.RGBA{R: 202, G: 165, B: 109, A: 255},
|
||||
color.RGBA{R: 190, G: 149, B: 93, A: 255},
|
||||
color.RGBA{R: 174, G: 133, B: 76, A: 255},
|
||||
color.RGBA{R: 157, G: 117, B: 60, A: 255},
|
||||
color.RGBA{R: 141, G: 105, B: 48, A: 255},
|
||||
color.RGBA{R: 129, G: 93, B: 36, A: 255},
|
||||
color.RGBA{R: 113, G: 76, B: 28, A: 255},
|
||||
color.RGBA{R: 97, G: 64, B: 16, A: 255},
|
||||
color.RGBA{R: 85, G: 52, B: 12, A: 255},
|
||||
color.RGBA{R: 68, G: 40, B: 4, A: 255},
|
||||
color.RGBA{R: 52, G: 32, B: 0, A: 255},
|
||||
color.RGBA{R: 36, G: 20, B: 0, A: 255},
|
||||
color.RGBA{R: 24, G: 12, B: 0, A: 255},
|
||||
color.RGBA{R: 255, G: 230, B: 186, A: 255},
|
||||
color.RGBA{R: 238, G: 210, B: 161, A: 255},
|
||||
color.RGBA{R: 226, G: 190, B: 141, A: 255},
|
||||
color.RGBA{R: 214, G: 170, B: 121, A: 255},
|
||||
color.RGBA{R: 202, G: 149, B: 105, A: 255},
|
||||
color.RGBA{R: 186, G: 133, B: 89, A: 255},
|
||||
color.RGBA{R: 174, G: 113, B: 72, A: 255},
|
||||
color.RGBA{R: 161, G: 93, B: 60, A: 255},
|
||||
color.RGBA{R: 145, G: 76, B: 48, A: 255},
|
||||
color.RGBA{R: 133, G: 60, B: 36, A: 255},
|
||||
color.RGBA{R: 121, G: 44, B: 24, A: 255},
|
||||
color.RGBA{R: 109, G: 32, B: 16, A: 255},
|
||||
color.RGBA{R: 93, G: 20, B: 8, A: 255},
|
||||
color.RGBA{R: 80, G: 8, B: 4, A: 255},
|
||||
color.RGBA{R: 68, G: 4, B: 0, A: 255},
|
||||
color.RGBA{R: 56, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 218, G: 218, B: 198, A: 255},
|
||||
color.RGBA{R: 194, G: 194, B: 170, A: 255},
|
||||
color.RGBA{R: 170, G: 170, B: 145, A: 255},
|
||||
color.RGBA{R: 145, G: 145, B: 121, A: 255},
|
||||
color.RGBA{R: 125, G: 125, B: 101, A: 255},
|
||||
color.RGBA{R: 101, G: 101, B: 76, A: 255},
|
||||
color.RGBA{R: 76, G: 76, B: 56, A: 255},
|
||||
color.RGBA{R: 56, G: 56, B: 40, A: 255},
|
||||
color.RGBA{R: 246, G: 222, B: 206, A: 255},
|
||||
color.RGBA{R: 234, G: 206, B: 190, A: 255},
|
||||
color.RGBA{R: 226, G: 194, B: 170, A: 255},
|
||||
color.RGBA{R: 214, G: 178, B: 153, A: 255},
|
||||
color.RGBA{R: 206, G: 165, B: 141, A: 255},
|
||||
color.RGBA{R: 194, G: 153, B: 125, A: 255},
|
||||
color.RGBA{R: 186, G: 141, B: 113, A: 255},
|
||||
color.RGBA{R: 174, G: 129, B: 101, A: 255},
|
||||
color.RGBA{R: 165, G: 117, B: 89, A: 255},
|
||||
color.RGBA{R: 153, G: 109, B: 76, A: 255},
|
||||
color.RGBA{R: 145, G: 97, B: 64, A: 255},
|
||||
color.RGBA{R: 133, G: 89, B: 56, A: 255},
|
||||
color.RGBA{R: 125, G: 76, B: 48, A: 255},
|
||||
color.RGBA{R: 113, G: 68, B: 36, A: 255},
|
||||
color.RGBA{R: 105, G: 60, B: 32, A: 255},
|
||||
color.RGBA{R: 93, G: 52, B: 24, A: 255},
|
||||
color.RGBA{R: 85, G: 44, B: 16, A: 255},
|
||||
color.RGBA{R: 72, G: 36, B: 12, A: 255},
|
||||
color.RGBA{R: 64, G: 32, B: 8, A: 255},
|
||||
color.RGBA{R: 56, G: 24, B: 4, A: 255},
|
||||
color.RGBA{R: 44, G: 16, B: 0, A: 255},
|
||||
color.RGBA{R: 36, G: 12, B: 0, A: 255},
|
||||
color.RGBA{R: 24, G: 8, B: 0, A: 255},
|
||||
color.RGBA{R: 16, G: 4, B: 0, A: 255},
|
||||
color.RGBA{R: 255, G: 255, B: 234, A: 255},
|
||||
color.RGBA{R: 255, G: 250, B: 198, A: 255},
|
||||
color.RGBA{R: 255, G: 242, B: 165, A: 255},
|
||||
color.RGBA{R: 255, G: 234, B: 129, A: 255},
|
||||
color.RGBA{R: 255, G: 218, B: 97, A: 255},
|
||||
color.RGBA{R: 255, G: 202, B: 60, A: 255},
|
||||
color.RGBA{R: 255, G: 182, B: 28, A: 255},
|
||||
color.RGBA{R: 255, G: 157, B: 0, A: 255},
|
||||
color.RGBA{R: 222, G: 222, B: 234, A: 255},
|
||||
color.RGBA{R: 202, G: 202, B: 218, A: 255},
|
||||
color.RGBA{R: 182, G: 182, B: 202, A: 255},
|
||||
color.RGBA{R: 165, G: 165, B: 186, A: 255},
|
||||
color.RGBA{R: 149, G: 149, B: 170, A: 255},
|
||||
color.RGBA{R: 133, G: 133, B: 157, A: 255},
|
||||
color.RGBA{R: 117, G: 117, B: 141, A: 255},
|
||||
color.RGBA{R: 101, G: 101, B: 125, A: 255},
|
||||
color.RGBA{R: 85, G: 85, B: 109, A: 255},
|
||||
color.RGBA{R: 72, G: 72, B: 97, A: 255},
|
||||
color.RGBA{R: 60, G: 60, B: 80, A: 255},
|
||||
color.RGBA{R: 44, G: 44, B: 64, A: 255},
|
||||
color.RGBA{R: 32, G: 32, B: 48, A: 255},
|
||||
color.RGBA{R: 20, G: 20, B: 32, A: 255},
|
||||
color.RGBA{R: 12, G: 12, B: 24, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
color.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
Palettes["WagesOfWar"] = WagesOfWarPalette
|
||||
}
|
@@ -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
|
@@ -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 |
@@ -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
|
Binary file not shown.
@@ -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.
@@ -27,7 +27,7 @@ var (
|
||||
|
||||
EOF
|
||||
|
||||
bytes.read(3) # Ignore idx 0 so we can make it transparent
|
||||
f.read(3) # Ignore idx 0 so we can make it transparent
|
||||
|
||||
255.times do
|
||||
r, g, b = f.read(3).bytes
|
||||
|
Reference in New Issue
Block a user