Compare commits
2 Commits
f971ba320c
...
soldiers-a
Author | SHA1 | Date | |
---|---|---|---|
494fe4eb02 | |||
30d1786e64 |
@@ -139,15 +139,16 @@ func loadMapsFrom(mapsPath string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Maps in %s:", mapsPath)
|
log.Printf("Maps in %s:", mapsPath)
|
||||||
for key, gm := range gameMaps {
|
for key, gameMap := range gameMaps {
|
||||||
rect := gm.Rect()
|
rect := gameMap.Rect()
|
||||||
|
hdr := gameMap.Header
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
" * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n",
|
" * `%s`: IsCampaignMap=%v W=%v:%v L=%v:%v SetName=%s\n",
|
||||||
key,
|
key,
|
||||||
gm.IsCampaignMap,
|
hdr.IsCampaignMap,
|
||||||
rect.Min.X, rect.Max.X,
|
rect.Min.X, rect.Max.X,
|
||||||
rect.Min.Y, rect.Max.Y,
|
rect.Min.Y, rect.Max.Y,
|
||||||
string(gm.SetName),
|
string(hdr.SetName[:]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -181,8 +181,8 @@ func (e *env) Draw(screen *ebiten.Image) error {
|
|||||||
|
|
||||||
for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
|
for y := int(rect.Min.Y); y < int(rect.Max.Y); y++ {
|
||||||
for x := int(rect.Min.X); x < int(rect.Max.X); x++ {
|
for x := int(rect.Min.X); x < int(rect.Max.X); x++ {
|
||||||
cell := gameMap.At(x, y, int(e.state.zIdx))
|
cell := gameMap.Cells.At(x, y, int(e.state.zIdx))
|
||||||
imd.Set(x, y, makeColour(cell, e.state.cellIdx))
|
imd.Set(x, y, makeColour(&cell, e.state.cellIdx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -598,8 +598,7 @@ Ignoring them for now, here's a first guess at a header:
|
|||||||
| 24 | 2 | ??? - varies. Seems related to character/squad position? |
|
| 24 | 2 | ??? - varies. Seems related to character/squad position? |
|
||||||
| 26 | 2 | ??? - invariant `00 04` |
|
| 26 | 2 | ??? - invariant `00 04` |
|
||||||
| 28 | 4 | ??? - varies (0 vs 5) |
|
| 28 | 4 | ??? - varies (0 vs 5) |
|
||||||
| 32 | 1 | Number of thingies |
|
| 32 | 4 | Number of thingies (26 vs 1) |
|
||||||
| 33 | 3 | ???. With a Lord of Change on the map, only one byte works as thingies count |
|
|
||||||
| 36 | 20 | Padding? |
|
| 36 | 20 | Padding? |
|
||||||
|
|
||||||
56 bytes of data is interesting because the value of that first, ignored byte is
|
56 bytes of data is interesting because the value of that first, ignored byte is
|
||||||
@@ -626,78 +625,7 @@ 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
|
mission title starts at 0x3B (59). So we can say we have 20 bytes of padding as
|
||||||
a first approximation?
|
a first approximation?
|
||||||
|
|
||||||
Here's where we're at with the per-character data, going from the padding values
|
The "trailer trailer", for want of a better term, seems to be organised as:
|
||||||
suggested above:
|
|
||||||
|
|
||||||
| Offset | Size | Meaning |
|
|
||||||
| ------ | ---- | ------- |
|
|
||||||
| 0 | 179 | ??? |
|
|
||||||
| 179 | 80 | Character name |
|
|
||||||
| 259 | 10 | Character attributes |
|
|
||||||
| 269 | 495 | ??? |
|
|
||||||
| 764 | 1(?) | Squad number? |
|
|
||||||
| 765 | 927 | ??? |
|
|
||||||
|
|
||||||
There's still a lot of bytes to dig through, but this allows me to load the
|
|
||||||
character names from Chapter01 correctly, with the exception of record 57 which
|
|
||||||
just contains `\x02` and is then null-terminated all the way through. Looking
|
|
||||||
at the bytes for one character record, I can easily correlate certain bytes to
|
|
||||||
various attributes; that's just done in code for the moment.
|
|
||||||
|
|
||||||
Given two characters of the same time, just in different locations, differing
|
|
||||||
values are seen at:
|
|
||||||
|
|
||||||
* `0x103 - 0x10c` (hodgepodge)
|
|
||||||
* `0x178 - 0x1be` (hodgepodge)
|
|
||||||
* `0x2fc` (0, 1) - squad number?
|
|
||||||
|
|
||||||
In Chapter01, picking a random character (Gorgon) and looking at his squadmates,
|
|
||||||
they are all in the same squad, and no other characters are in that squad, so it
|
|
||||||
looks pretty diagnostic to me. There's nothing in the UI to indicate the squad,
|
|
||||||
though.
|
|
||||||
|
|
||||||
Now let's look for position. In my 2-character map, they're at 65,50 and 70,55.
|
|
||||||
Within a character, I see those numbers repeated twice - around `0x1b{9,a}` and
|
|
||||||
`0x1b{d,e}`.
|
|
||||||
|
|
||||||
Characters don't seem to take up multiple x,y squares, but they *can* take up
|
|
||||||
multiple Z levels. So maybe this is a bounding box of some kind?
|
|
||||||
|
|
||||||
Adding a (tall) Lord of Change to the map gave me `02 36 45 00 02 37 45`, which
|
|
||||||
doesn't quite match what my eyes are telling me for Z,Y,X. In addition, the data
|
|
||||||
immediately after this offset changed into a large number of coordinate-like
|
|
||||||
sets of values - far too many for it to actually be a bounding box. However, the
|
|
||||||
first one remains good as a position specifier.
|
|
||||||
|
|
||||||
Down in `0x679` (Chaos Sorcerer) or `0x68D` (Lord of Change), the map coords for
|
|
||||||
the *other* character appears, which is downright odd. For now, just use the
|
|
||||||
first-indexed value.
|
|
||||||
|
|
||||||
How about their types? We need to be able to work out which animations to show,
|
|
||||||
so they must be uniquely identified somehow. Ultramarine captain is animation
|
|
||||||
group 12, while chaos lord is group... 14? Not certain. I don't see either of
|
|
||||||
those numbers in a promising spot, anyway.
|
|
||||||
|
|
||||||
|
|
||||||
I do see differences at around `0x170`:
|
|
||||||
|
|
||||||
```
|
|
||||||
Ultramarine Captain: 00 00 00 00 00 00 00 00 50 03 00 00 00 00 00 00
|
|
||||||
Chaos Lord: 00 00 00 00 11 01 00 00 47 03 00 04 00 00 02 00
|
|
||||||
```
|
|
||||||
|
|
||||||
Maybe they're somewhat relevant, it's hard to say.
|
|
||||||
|
|
||||||
Thingies next: these aren't decoded at all yet, and the sizes seem to be
|
|
||||||
variable.
|
|
||||||
|
|
||||||
| Offset | Size | Meaning |
|
|
||||||
| ------ | ---- | ------- |
|
|
||||||
| | | |
|
|
||||||
|
|
||||||
|
|
||||||
Finally, the "trailer trailer", for want of a better term, seems to be organised
|
|
||||||
as:
|
|
||||||
|
|
||||||
| Offset | Size | Meaning |
|
| Offset | Size | Meaning |
|
||||||
| ----- | ---- | ------- |
|
| ----- | ---- | ------- |
|
||||||
@@ -705,8 +633,6 @@ as:
|
|||||||
| 255 | 2048 | Briefing |
|
| 255 | 2048 | Briefing |
|
||||||
| 2304 | 85 | ??? - each byte is 1 or 0. Spaced so it may be partly uint32 |
|
| 2304 | 85 | ??? - each byte is 1 or 0. Spaced so it may be partly uint32 |
|
||||||
|
|
||||||
This duplicates the information found in the `.TXT` files. No idea what the end
|
|
||||||
data is yet.
|
|
||||||
|
|
||||||
## Soldiers At War
|
## Soldiers At War
|
||||||
|
|
||||||
|
3
go.mod
3
go.mod
@@ -8,9 +8,7 @@ require (
|
|||||||
github.com/hajimehoshi/ebiten v1.11.1
|
github.com/hajimehoshi/ebiten v1.11.1
|
||||||
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
|
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4
|
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4
|
||||||
github.com/stretchr/testify v1.5.1
|
github.com/stretchr/testify v1.5.1
|
||||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
|
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
|
||||||
@@ -18,5 +16,4 @@ require (
|
|||||||
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect
|
golang.org/x/mobile v0.0.0-20200329125638-4c31acba0007 // indirect
|
||||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
|
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c
|
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@@ -40,13 +40,9 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe h1:ewr1srjRCmcQogPQ/NCx6XCk6LGVmsVCc9Y3vvPZj+Y=
|
|
||||||
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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 h1:Y/KOCu+ZLB730PudefxfsKVjtI0m0RhvFk9a0l4O1+c=
|
||||||
@@ -111,7 +107,5 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
|
|||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c h1:7j7Yy/3gedviEts3jKY0bEruQkTFKh+8pDmEFaM6UBc=
|
|
||||||
gopkg.in/restruct.v1 v1.0.0-20190323193435-3c2afb705f3c/go.mod h1:WJaLhyHHEQFOgwIxu/SJxvUHJA18glYsMETBTMIySTY=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
@@ -47,7 +47,7 @@ func (a *AssetStore) Map(name string) (*Map, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := &Map{
|
m := &Map{
|
||||||
Rect: raw.Rect(),
|
Rect: raw.Rect(),
|
||||||
assets: a,
|
assets: a,
|
||||||
raw: raw,
|
raw: raw,
|
||||||
set: set,
|
set: set,
|
||||||
@@ -74,8 +74,8 @@ func (m *Map) LoadSprites() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: get rid of this
|
// FIXME: get rid of this
|
||||||
func (m *Map) Cell(x, y, z int) *maps.Cell {
|
func (m *Map) Cell(x, y, z int) maps.Cell {
|
||||||
return m.raw.At(x, y, z)
|
return m.raw.Cells.At(x, y, z)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpritesForCell returns the sprites needed to correctly render this cell.
|
// SpritesForCell returns the sprites needed to correctly render this cell.
|
||||||
@@ -102,21 +102,5 @@ func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) {
|
|||||||
sprites = append(sprites, sprite)
|
sprites = append(sprites, sprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: this just marks character positions with sprite 19 for now.
|
|
||||||
specialsObj, err := m.assets.Object("specials")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
chrSpr, err := specialsObj.Sprite(19)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, chr := range m.raw.Characters {
|
|
||||||
if chr.XPos == x && chr.YPos == y && z == 1 { // FIXME: sort out ZPos
|
|
||||||
sprites = append(sprites, chrSpr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprites, nil
|
return sprites, nil
|
||||||
}
|
}
|
||||||
|
@@ -6,17 +6,17 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/lunixbochs/struc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
expectedMagic = []byte("\x08\x00WHMAP\x00")
|
expectedMagic = []byte("\x15\x00AMB_MAP\x00")
|
||||||
expectedSetNameOffset = uint32(0x34)
|
expectedSetNameOffset = uint32(0x10)
|
||||||
notImplemented = fmt.Errorf("Not implemented")
|
notImplemented = fmt.Errorf("Not implemented")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,134 +25,77 @@ const (
|
|||||||
MaxLength = 100 // Y coordinate
|
MaxLength = 100 // Y coordinate
|
||||||
MaxWidth = 130 // X coordinate
|
MaxWidth = 130 // X coordinate
|
||||||
|
|
||||||
CellSize = 16 // seems to be
|
CellSize = 13 // seems to be
|
||||||
|
|
||||||
cellDataOffset = 0x110 // definitely
|
cellDataOffset = 0xc0
|
||||||
cellCount = MaxHeight * MaxLength * MaxWidth
|
cellCount = MaxHeight * MaxLength * MaxWidth
|
||||||
)
|
)
|
||||||
|
|
||||||
type GameMap struct {
|
type Header struct {
|
||||||
// Main Header
|
Magic [10]byte // "\x15\x00AMB_MAP\x00"
|
||||||
IsCampaignMap bool `struc:"uint32"` // Tentatively: 0 = no, 1 = yes
|
SetName [8]byte // Links to a filename in `/Sets/*.set`
|
||||||
MinWidth int `struc:"uint32"`
|
// Need to investigate the rest of the header too
|
||||||
MinLength int `struc:"uint32"`
|
IsCampaignMap byte
|
||||||
MaxWidth int `struc:"uint32"`
|
|
||||||
MaxLength int `struc:"uint32"`
|
|
||||||
Unknown1 int `struc:"uint32"`
|
|
||||||
Unknown2 int `struc:"uint32"`
|
|
||||||
Unknown3 int `struc:"uint32"`
|
|
||||||
Unknown4 int `struc:"uint32"`
|
|
||||||
Magic []byte `struc:"[8]byte"` // "\x08\x00WHMAP\x00"
|
|
||||||
Unknown5 int `struc:"uint32"`
|
|
||||||
Unknown6 int `struc:"uint32"`
|
|
||||||
SetName string `struc:"[8]byte"` // Links to a filename in `/Sets/*.set`
|
|
||||||
Padding []byte `struc:"[212]byte"`
|
|
||||||
|
|
||||||
// Per-cell data
|
|
||||||
NumCells int `struc:"skip"` // FIXME: We can't use []Cell below without this field
|
|
||||||
Cells []Cell `struc:"[]Cell,sizefrom=NumCells"`
|
|
||||||
|
|
||||||
// Trailer header
|
|
||||||
Discard1 [3]byte `struc:"[3]byte"` // First byte is size of trailer header?
|
|
||||||
|
|
||||||
TrailerMaxWidth int `struc:"uint32"`
|
|
||||||
TrailerMaxLength int `struc:"uint32"`
|
|
||||||
TrailerMinWidth int `struc:"uint32"`
|
|
||||||
TrailerMinLength int `struc:"uint32"`
|
|
||||||
|
|
||||||
NumCharacters int `struc:"uint32"`
|
|
||||||
|
|
||||||
TrailerUnknown1 int `struc:"uint32"`
|
|
||||||
TrailerUnknown2 int `struc:"uint16"`
|
|
||||||
TrailerUnknown3 int `struc:"uint16"`
|
|
||||||
TrailerUnknown4 int `struc:"uint32"`
|
|
||||||
|
|
||||||
NumThingies int `struc:"byte"`
|
|
||||||
TrailerUnknown5 []byte `struc:"[3]byte"`
|
|
||||||
Padding1 []byte `struc:"[20]byte"`
|
|
||||||
|
|
||||||
// FIXME: The rest is trash until Character & Thingy are worked out
|
|
||||||
|
|
||||||
Characters []Character `struc:"[]Character,sizefrom=NumCharacters"`
|
|
||||||
Thingies []Thingy `struc:"[]Thingy,sizefrom=NumThingies"`
|
|
||||||
|
|
||||||
Title string `struc:"[255]byte"`
|
|
||||||
Briefing string `struc:"[2048]byte"`
|
|
||||||
|
|
||||||
// Maybe? each contains either 0 or 1? Hard to say
|
|
||||||
TrailerUnknown6 []byte `struc:"[85]byte"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cell struct {
|
type TrailerHeader struct {
|
||||||
DoorAndCanisterRelated byte `struc:"byte"`
|
Discard1 [3]byte // No idea what this lot is
|
||||||
DoorLockAndReactorRelated byte `struc:"byte"`
|
MaxWidth uint32
|
||||||
Unknown2 byte `struc:"byte"`
|
MaxLength uint32
|
||||||
Surface ObjRef
|
MinWidth uint32
|
||||||
Left ObjRef
|
MinLength uint32
|
||||||
Right ObjRef
|
|
||||||
Center ObjRef
|
NumCharacters uint32
|
||||||
Unknown11 byte `struc:"byte"`
|
|
||||||
Unknown12 byte `struc:"byte"`
|
Unknown1 uint32
|
||||||
Unknown13 byte `struc:"byte"`
|
Unknown2 uint16
|
||||||
Unknown14 byte `struc:"byte"`
|
Unknown3 uint16
|
||||||
SquadRelated byte `struc:"byte"`
|
Unknown4 uint32
|
||||||
|
NumThingies uint32
|
||||||
|
Padding1 [20]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrailerTrailer struct {
|
||||||
|
Title [255]byte
|
||||||
|
Briefing [2048]byte
|
||||||
|
Unknown1 [85]uint8 // Maybe? each contains either 0 or 1? Hard to say
|
||||||
}
|
}
|
||||||
|
|
||||||
type Character struct {
|
type Character struct {
|
||||||
Unknown1 []byte `struc:"[179]byte"`
|
Unknown1 uint32
|
||||||
Name string `struc:"[80]byte"`
|
|
||||||
|
|
||||||
// Attributes guessed by matching up numbers. Starts at 0x103
|
|
||||||
WeaponSkill int `struc:"byte"`
|
|
||||||
BallisticSkill int `struc:"byte"`
|
|
||||||
Unknown2 byte `struc:"byte"`
|
|
||||||
Leadership int `struc:"byte"`
|
|
||||||
Toughness int `struc:"byte"`
|
|
||||||
Strength int `struc:"byte"`
|
|
||||||
ActionPoints int `struc:"byte"`
|
|
||||||
Unknown3 byte `struc:"byte"`
|
|
||||||
Unknown4 byte `struc:"byte"`
|
|
||||||
Health int `struc:"byte"`
|
|
||||||
|
|
||||||
Unknown5 []byte `struc:"[91]byte"`
|
|
||||||
Armor int `struc:"byte"`
|
|
||||||
Unknown6 []byte `struc:"[84]byte"`
|
|
||||||
YPos int `struc:"byte"` // These are actually much more complicated
|
|
||||||
XPos int `struc:"byte"`
|
|
||||||
Unknown7 []byte `struc:"[317]byte"`
|
|
||||||
SquadNumber byte `struc:"byte"`
|
|
||||||
Unknown8 []byte `struc:"[927]byte"`
|
|
||||||
// TODO: each character may have a fixed number of subrecords for inventory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Characters []Character
|
type Characters []Character
|
||||||
|
|
||||||
// TODO. These are triggers/reactors/etc.
|
// TODO. These are triggers/reactors/etc.
|
||||||
type Thingy struct {
|
type Thingy struct {}
|
||||||
Unknown1 int `struc:"uint32"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Thingies []Thingy
|
type Thingies []Thingy
|
||||||
|
|
||||||
func (g *GameMap) MapSetName() string {
|
func (h Header) MapSetName() string {
|
||||||
return g.SetName
|
idx := bytes.IndexByte(h.SetName[:], 0)
|
||||||
|
if idx < 0 {
|
||||||
|
idx = 8 // all 8 bytes are used
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(h.SetName[0:idx:idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GameMap) MapSetFilename() string {
|
func (h Header) MapSetFilename() string {
|
||||||
return g.MapSetName() + ".set"
|
return h.MapSetName() + ".set"
|
||||||
}
|
}
|
||||||
|
|
||||||
type ObjRef struct {
|
type ObjRef struct {
|
||||||
AreaByte byte `struc:"byte"`
|
AreaByte byte
|
||||||
SpriteAndFlagByte byte `struc:"byte"`
|
SpriteAndFlagByte byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// The index into a set palette to retrieve the object
|
// The index into a set palette to retrieve the object
|
||||||
func (o *ObjRef) Index() int {
|
func (o ObjRef) Index() int {
|
||||||
return int(o.AreaByte)
|
return int(o.AreaByte & 0x7f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ObjRef) Sprite() int {
|
func (o ObjRef) Sprite() int {
|
||||||
// The top bit seems to be a flag of some kind
|
// The top bit seems to be a flag of some kind
|
||||||
return int(o.SpriteAndFlagByte & 0x7f)
|
return int(o.SpriteAndFlagByte & 0x7f)
|
||||||
}
|
}
|
||||||
@@ -160,67 +103,128 @@ func (o *ObjRef) Sprite() int {
|
|||||||
// The top bit seems to say whether we should draw or not.
|
// The top bit seems to say whether we should draw or not.
|
||||||
func (o ObjRef) IsActive() bool {
|
func (o ObjRef) IsActive() bool {
|
||||||
return (o.SpriteAndFlagByte & 0x80) == 0x80
|
return (o.SpriteAndFlagByte & 0x80) == 0x80
|
||||||
|
} // PARIS is 78 x 60 x 7
|
||||||
|
// 4E 3C 7
|
||||||
|
/*
|
||||||
|
type Cell struct {
|
||||||
|
DoorAndCanisterRelated byte
|
||||||
|
// DoorLockAndReactorRelated byte
|
||||||
|
// Unknown2 byte
|
||||||
|
Surface ObjRef
|
||||||
|
Left ObjRef
|
||||||
|
Right ObjRef
|
||||||
|
Center ObjRef
|
||||||
|
Unknown11 byte
|
||||||
|
Unknown12 byte
|
||||||
|
Unknown13 byte
|
||||||
|
Unknown14 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 {
|
func (c *Cell) At(n int) byte {
|
||||||
switch n {
|
switch n {
|
||||||
case 0:
|
case 0:
|
||||||
return c.DoorAndCanisterRelated
|
return c.Unknown1
|
||||||
case 1:
|
case 1:
|
||||||
return c.DoorLockAndReactorRelated
|
|
||||||
case 2:
|
|
||||||
return c.Unknown2
|
|
||||||
case 3:
|
|
||||||
return c.Surface.AreaByte
|
return c.Surface.AreaByte
|
||||||
case 4:
|
case 2:
|
||||||
return c.Surface.SpriteAndFlagByte
|
return c.Surface.SpriteAndFlagByte
|
||||||
case 5:
|
case 3:
|
||||||
return c.Left.AreaByte
|
return c.Left.AreaByte
|
||||||
case 6:
|
case 4:
|
||||||
return c.Left.SpriteAndFlagByte
|
return c.Left.SpriteAndFlagByte
|
||||||
case 7:
|
case 5:
|
||||||
return c.Right.AreaByte
|
return c.Right.AreaByte
|
||||||
case 8:
|
case 6:
|
||||||
return c.Right.SpriteAndFlagByte
|
return c.Right.SpriteAndFlagByte
|
||||||
case 9:
|
case 7:
|
||||||
return c.Center.AreaByte
|
return c.Center.AreaByte
|
||||||
case 10:
|
case 8:
|
||||||
return c.Center.SpriteAndFlagByte
|
return c.Center.SpriteAndFlagByte
|
||||||
|
case 9:
|
||||||
|
return c.Unknown2[0]
|
||||||
|
case 10:
|
||||||
|
return c.Unknown2[1]
|
||||||
case 11:
|
case 11:
|
||||||
return c.Unknown11
|
return c.Unknown2[2]
|
||||||
case 12:
|
case 12:
|
||||||
return c.Unknown12
|
return c.Unknown2[3]
|
||||||
case 13:
|
|
||||||
return c.Unknown13
|
|
||||||
case 14:
|
|
||||||
return c.Unknown14
|
|
||||||
case 15:
|
|
||||||
return c.SquadRelated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GameMap) At(x, y, z int) *Cell {
|
// Cells is always a fixed size; use At to get a cell according to x,y,z
|
||||||
return &g.Cells[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
|
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 {
|
||||||
|
// log.Printf("At (%v,%v,%v)=%v", x, y, z, x*y*z)
|
||||||
|
return c[(z*MaxLength*MaxWidth)+
|
||||||
|
(y*MaxWidth)+
|
||||||
|
x]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GameMap) Check() error {
|
func (h Header) Check() []error {
|
||||||
if bytes.Compare(expectedMagic, g.Magic) != 0 {
|
var out []error
|
||||||
return fmt.Errorf("Unexpected magic value: %v", g.Magic)
|
// 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: other consistency checks
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
type GameMap struct {
|
||||||
|
Header
|
||||||
|
Cells
|
||||||
|
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 {
|
func (m *GameMap) Rect() image.Rectangle {
|
||||||
return image.Rect(
|
return image.Rect(
|
||||||
int(m.MinWidth),
|
int(m.TrailerHeader.MinWidth),
|
||||||
int(m.MinLength),
|
int(m.TrailerHeader.MinLength),
|
||||||
int(m.MaxWidth),
|
int(m.TrailerHeader.MaxWidth-1),
|
||||||
int(m.MaxLength),
|
int(m.TrailerHeader.MaxLength-1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,17 +247,23 @@ func LoadGameMap(prefix string) (*GameMap, error) {
|
|||||||
return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix)
|
return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A game map is composed of two files: .map and .txt. We ignore the text file,
|
// A game map is composed of two files: .map and .txt
|
||||||
// since the content is replicated in the map file.
|
|
||||||
func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
|
func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
|
||||||
out, err := loadMapFile(mapFile)
|
out, err := loadMapFile(mapFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := out.Check(); err != nil {
|
// TODO: load text and parse into sections
|
||||||
|
txt, err := ioutil.ReadFile(txtFile)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
out.Text = string(txt)
|
||||||
|
|
||||||
|
for _, err := range out.Check() {
|
||||||
|
log.Printf("%s: %v", mapFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -291,7 +301,6 @@ func LoadGameMaps(dir string) (map[string]*GameMap, error) {
|
|||||||
|
|
||||||
func loadMapFile(filename string) (*GameMap, error) {
|
func loadMapFile(filename string) (*GameMap, error) {
|
||||||
var out GameMap
|
var out GameMap
|
||||||
out.NumCells = cellCount
|
|
||||||
|
|
||||||
mf, err := os.Open(filename)
|
mf, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -307,43 +316,57 @@ func loadMapFile(filename string) (*GameMap, error) {
|
|||||||
|
|
||||||
defer zr.Close()
|
defer zr.Close()
|
||||||
|
|
||||||
if err := struc.UnpackWithOrder(zr, &out, binary.LittleEndian); err != nil {
|
if err := binary.Read(zr, binary.LittleEndian, &out.Header); err != nil {
|
||||||
|
return nil, fmt.Errorf("Error parsing %s: %v", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// no gzip.SeekReader, so discard unread header bytes for now
|
||||||
|
discardSize := int64(cellDataOffset - binary.Size(&out.Header))
|
||||||
|
if _, err := io.CopyN(ioutil.Discard, zr, discardSize); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim any trailing nulls off of the strings
|
out.Cells = make(Cells, cellCount)
|
||||||
nullTerminate(&out.SetName)
|
if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil {
|
||||||
nullTerminate(&out.Title)
|
return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err)
|
||||||
nullTerminate(&out.Briefing)
|
|
||||||
|
|
||||||
for i, chr := range out.Characters {
|
|
||||||
nullTerminate(&chr.Name)
|
|
||||||
fmt.Printf("Character %v: %s\n", i, chr.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Mission Title: %q\n", out.Title)
|
// no gzip.SeekReader, so discard unread trailer bytes for now
|
||||||
fmt.Printf("Mission Briefing: %q\n", out.Briefing)
|
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
|
return &out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nullTerminate(s *string) {
|
func (t *TrailerTrailer) String() string {
|
||||||
sCpy := *s
|
|
||||||
idx := strings.Index(sCpy, "\x00")
|
|
||||||
if idx < 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*s = sCpy[0:idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Character) String() string {
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"squad=%v pos=(%v,%v) name=%q\n\t%3d %3d %3d %3d %3d\n\t%3d %3d ??? ??? %3d\n",
|
"title=%q briefing=%q rest=%#+v",
|
||||||
c.SquadNumber,
|
strings.TrimRight(string(t.Title[:]), "\x00"),
|
||||||
c.XPos, c.YPos,
|
strings.TrimRight(string(t.Briefing[:]), "\x00"),
|
||||||
c.Name,
|
t.Unknown1,
|
||||||
c.ActionPoints, c.Health, c.Armor, c.BallisticSkill, c.WeaponSkill,
|
|
||||||
c.Strength, c.Toughness /*c.Initiative, c.Attacks,*/, c.Leadership,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ type CellPoint struct {
|
|||||||
Z int
|
Z int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scenario) CellAtCursor() (*maps.Cell, CellPoint) {
|
func (s *Scenario) CellAtCursor() (maps.Cell, CellPoint) {
|
||||||
cell := s.area.Cell(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
|
cell := s.area.Cell(int(s.selectedCell.X), int(s.selectedCell.Y), 0)
|
||||||
return cell, CellPoint{IsoPt: s.selectedCell, Z: 0}
|
return cell, CellPoint{IsoPt: s.selectedCell, Z: 0}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user