Compare commits

...

13 Commits

Author SHA1 Message Date
494fe4eb02 Trim map based on trailer data 2020-06-08 00:53:45 +01:00
30d1786e64 Get SaW maps displaying 2020-06-08 00:48:33 +01:00
65bae80d40 Add a note about SaW trailer 2020-06-08 00:48:19 +01:00
e8e9811b5d More map trailer work 2020-06-08 00:24:57 +01:00
a6fdbaef2b Make some progress decoding map trailer 2020-06-07 01:44:28 +01:00
0bf8233cd1 Fix binary paths in README 2020-06-06 12:45:10 +01:00
c2cbf1d95d Get the initial copyright notice displaying
This is really awful, but it's nice to check it off.
2020-06-06 12:44:08 +01:00
54fe95239e More README niceness 2020-06-05 22:47:06 +01:00
63d3ee0ed6 Update README.md a bit 2020-06-01 01:55:55 +01:00
5c869fc33c Make some Wages of War sprites displayable 2020-06-01 01:43:03 +01:00
4358951e15 Add the Wages of War palette (first guess) 2020-06-01 01:41:45 +01:00
250a6033c8 Fix an error in the palette generator 2020-06-01 01:41:24 +01:00
f64af717b7 Fix format strings 2020-06-01 01:32:03 +01:00
17 changed files with 720 additions and 110 deletions

1
.gitignore vendored
View File

@@ -5,4 +5,5 @@
/SL
/SaW
/WoW
/WoW-CD
/bin

View File

@@ -32,6 +32,8 @@ 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
@@ -45,6 +47,35 @@ 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
@@ -69,7 +100,8 @@ 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
@@ -87,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:
@@ -118,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
@@ -153,4 +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.

View File

@@ -58,7 +58,7 @@ func main() {
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatal("Failed to set up asset store: %v", err)
log.Fatalf("Failed to set up asset store: %v", err)
}
state := state{

View File

@@ -57,7 +57,7 @@ func main() {
assets, err := assetstore.New(cfg.DefaultEngine())
if err != nil {
log.Fatal("Failed to set up asset store: %v", err)
log.Fatalf("Failed to set up asset store: %v", err)
}
var obj *assetstore.Object

View File

@@ -3,7 +3,7 @@ video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--
default_engine = "ordoor"
[engines.geas] # Wages of War -> Gifts of Peace -> Geas
data_dir = "./WoW"
data_dir = "./WoW-CD"
palette = "WagesOfWar"
[engines.ordoor] # Chaos Gate -> Order Door -> Ordoor
@@ -14,9 +14,9 @@ default_engine = "ordoor"
data_dir = "./SaW"
palette = "SoldiersAtWar"
# [engines.] Squad Leader -> ??? -> ???
# data_dir = "./SL"
# palette = "SquadLeader" # may not be relevant?
[engines.sl] # Squad Leader -> ??? -> ???
data_dir = "./SL"
palette = "ChaosGate" # may not be relevant?
[options]
play_movies = true

View File

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

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

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

View File

@@ -8,6 +8,8 @@ import (
"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"
@@ -44,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
@@ -109,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)

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

View File

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

View File

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

View File

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

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

View File

@@ -7,6 +7,7 @@ package ordoor
import (
"fmt"
"log"
"time"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/audio"
@@ -29,6 +30,10 @@ 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
}
@@ -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
}

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

View File

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