Some more map progress

This commit is contained in:
2018-03-18 13:57:01 +00:00
parent 88acc05085
commit 2f02c7bbf3
5 changed files with 314 additions and 102 deletions

View File

@@ -123,5 +123,11 @@ func loadSets() {
for key, mapSet := range mapSets { for key, mapSet := range mapSets {
fmt.Printf(" * `%s`: Defs=%#v len(palette)=%d\n", key, mapSet.Defs, len(mapSet.Palette)) fmt.Printf(" * `%s`: Defs=%#v len(palette)=%d\n", key, mapSet.Defs, len(mapSet.Palette))
if key == "map01.set" {
for i, objName := range mapSet.Palette {
fmt.Printf(" %d. %s\n", i, objName)
}
}
} }
} }

View File

@@ -5,6 +5,7 @@ import (
"log" "log"
"math" "math"
"os" "os"
"path/filepath"
"time" "time"
"github.com/faiface/pixel" "github.com/faiface/pixel"
@@ -13,6 +14,7 @@ import (
"golang.org/x/image/colornames" "golang.org/x/image/colornames"
"ur.gs/chaos-gate/internal/maps" "ur.gs/chaos-gate/internal/maps"
"ur.gs/chaos-gate/internal/sets"
) )
var ( var (
@@ -24,6 +26,26 @@ var (
winY = flag.Int("win-y", 1024, "height of the view-map window") winY = flag.Int("win-y", 1024, "height of the view-map window")
) )
type env struct {
gameMap *maps.GameMap
set sets.MapSet
}
type runState struct {
env *env
autoUpdate bool
started time.Time
cam pixel.Matrix
camPos pixel.Vec
zoom float64
zIdx int
cellIdx int
}
func main() { func main() {
flag.Parse() flag.Parse()
@@ -37,23 +59,21 @@ func main() {
log.Fatalf("Couldn't load map file: %v", err) log.Fatalf("Couldn't load map file: %v", err)
} }
setFile := filepath.Join(*gamePath, "Sets", gameMap.MapSetFilename())
log.Println(setFile)
mapSet, err := sets.LoadSet(setFile)
if err != nil {
log.Fatalf("Couldn't load set file %s: %v", setFile, err)
}
env := &env{gameMap: gameMap, set: mapSet}
// The main thread now belongs to pixelgl // The main thread now belongs to pixelgl
pixelgl.Run(func() { run(gameMap) }) pixelgl.Run(func() { run(env) })
} }
type runState struct { func run(env *env) {
redraw bool // WARE! 0,0 is the *bottom left* of the window
started time.Time
autoUpdate bool
gameMap *maps.GameMap
zoom float64
zIdx int
cellIdx int
}
func run(gameMap *maps.GameMap) {
cfg := pixelgl.WindowConfig{ cfg := pixelgl.WindowConfig{
Title: "View Map " + *mapFile, Title: "View Map " + *mapFile,
Bounds: pixel.R(0, 0, float64(*winX), float64(*winY)), Bounds: pixel.R(0, 0, float64(*winX), float64(*winY)),
@@ -66,74 +86,141 @@ func run(gameMap *maps.GameMap) {
} }
state := &runState{ state := &runState{
redraw: true, env: env,
started: time.Now(),
autoUpdate: true, autoUpdate: true,
gameMap: gameMap,
zoom: 1.0, camPos: pixel.V(0, float64(-*winY)),
zoom: 8.0,
} }
for !win.Closed() { for !win.Closed() {
if state.redraw { oldState := *state
log.Printf("z=%d cellIdx=%d", state.zIdx, state.cellIdx)
presentFull(win, state)
}
state = runStep(win, state) state = runStep(win, state)
if oldState != *state {
log.Printf("z=%d cellIdx=%d", state.zIdx, state.cellIdx)
present(win, state)
}
win.Update() win.Update()
} }
} }
// Converts pixel coordinates to cell coordinates
func vecToCell(vec pixel.Vec) (int, int) {
x := int(vec.X)
y := int(vec.Y)
if x < 0 {
x = 0
}
if x > maps.MaxWidth-1 {
x = maps.MaxWidth - 1
}
if y < 0 {
y = 0
}
if y > maps.MaxLength-1 {
y = maps.MaxLength - 1
}
return x, y
}
func cellToVec(x, y int) pixel.Rect {
min := pixel.Vec{X: float64(x), Y: float64(y)}
max := pixel.Vec{X: min.X + 1, Y: min.Y + 1}
return pixel.Rect{Min: min, Max: max}
}
// TODO: cut this down to showing just the viewport? // TODO: cut this down to showing just the viewport?
// The naive approach using gameMap.Width() / Height() cuts half the map off :/ // The naive approach using gameMap.Width() / Height() cuts half the map off :/
func presentFull(win *pixelgl.Window, state *runState) { func present(win *pixelgl.Window, state *runState) {
gameMap := state.gameMap gameMap := state.env.gameMap
center := win.Bounds().Center()
sz := win.Bounds().Size()
imd := imdraw.New(nil) imd := imdraw.New(nil)
// Rotate everything 45' anticlockwise to get an isometric view with the for y := gameMap.MinLength; y < gameMap.MaxLength; y++ {
// lowest coordinates at the (now) top corner for x := gameMap.MinWidth; x < gameMap.MaxWidth; x++ {
imd.SetMatrix(pixel.IM.Rotated(center, -math.Pi/4)) rect := cellToVec(int(x), int(y))
xPerCell := sz.X / float64(maps.MaxWidth)
yPerCell := sz.Y / float64(maps.MaxLength)
for y := 0; y < maps.MaxLength; y++ {
for x := 0; x < maps.MaxWidth; x++ {
min := pixel.Vec{X: float64(x) * xPerCell, Y: float64(y) * yPerCell}
max := pixel.Vec{X: min.X + xPerCell, Y: min.Y + yPerCell}
cell := gameMap.Cells.At(int(x), int(y), int(state.zIdx)) cell := gameMap.Cells.At(int(x), int(y), int(state.zIdx))
// TODO: represent the state of the cell *sensibly*, using colour. // TODO: represent the state of the cell *sensibly*, using colour.
// Need to understand the contents better first, so for now optimize // Need to understand the contents better first, so for now optimize
// for exploration // for exploration
// imd.Color = makeColour(&cell, state.cellIdx)
// CellIndex=3 shows walls & elevation changes imd.Push(rect.Min, rect.Max)
imd.Color = makeColour(cell, state.cellIdx)
imd.Push(min, max)
imd.Rectangle(0.0) imd.Rectangle(0.0)
} }
} }
cam := pixel.IM.Scaled(center, state.zoom) // Draw the boundary
win.SetMatrix(cam) rect := pixel.R(
float64(gameMap.MinWidth)-1, float64(gameMap.MinLength)-1,
float64(gameMap.MaxWidth)+1, float64(gameMap.MaxLength)+1,
)
imd.Color = pixel.RGB(255, 0, 0)
imd.EndShape = imdraw.SharpEndShape
imd.Push(rect.Min, rect.Max)
imd.Rectangle(1.0)
center := win.Bounds().Center()
cam := pixel.IM
cam = cam.ScaledXY(pixel.ZV, pixel.Vec{1.0, -1.0}) // invert the Y axis
cam = cam.Scaled(pixel.ZV, state.zoom) // apply current zoom factor
cam = cam.Moved(center.Sub(state.camPos)) // Make it central
cam = cam.Rotated(center.Sub(state.camPos), -0.785) // Apply isometric angle
state.cam = cam
win.SetMatrix(state.cam)
win.Clear(colornames.Black) win.Clear(colornames.Black)
imd.Draw(win) imd.Draw(win)
} }
func makeColour(cell maps.Cell, colIdx int) pixel.RGBA { func makeColour(cell *maps.Cell, colIdx int) pixel.RGBA {
return pixel.RGB( var scale func(float64) float64
float64(cell[colIdx]),
float64(cell[colIdx]), mult := func(factor float64) func(float64) float64 {
float64(cell[colIdx]), return func(in float64) float64 { return in * factor }
) }
// Different columns do better with different levels of greyscale.
switch colIdx {
case 0:
scale = mult(0.004)
case 1:
scale = mult(0.1)
case 2:
scale = mult(1.0)
case 3:
scale = mult(0.1)
case 4:
scale = func(in float64) float64 { return mult(0.01)(in - 100) }
case 10:
scale = func(in float64) float64 { return mult(0.01)(in - 100) }
case 12:
scale = mult(0.004)
case 13:
scale = mult(0.004)
case 14:
scale = mult(0.004)
default:
scale = mult(0.01) // close to maximum resolution, low-value fields will be lost
}
col := scale(float64(cell.At(colIdx)))
return pixel.RGB(col, col, col)
} }
func runStep(win *pixelgl.Window, state *runState) *runState { func runStep(win *pixelgl.Window, state *runState) *runState {
nextState := *state nextState := *state
nextState.redraw = false
// Enable / disable auto-update with the enter key // Enable / disable auto-update with the enter key
if win.JustPressed(pixelgl.KeyEnter) { if win.JustPressed(pixelgl.KeyEnter) {
@@ -146,8 +233,7 @@ func runStep(win *pixelgl.Window, state *runState) *runState {
} }
// Automatically cycle every second when auto-update is on // Automatically cycle every second when auto-update is on
if nextState.autoUpdate && time.Now().Sub(nextState.started) > time.Second { if nextState.autoUpdate && time.Now().Sub(state.started) > 500*time.Millisecond {
nextState.redraw = true
nextState.cellIdx = nextState.cellIdx + 1 nextState.cellIdx = nextState.cellIdx + 1
if nextState.cellIdx >= maps.CellSize { if nextState.cellIdx >= maps.CellSize {
nextState.cellIdx = 0 nextState.cellIdx = 0
@@ -161,47 +247,61 @@ func runStep(win *pixelgl.Window, state *runState) *runState {
nextState.started = time.Now() nextState.started = time.Now()
} }
if win.JustPressed(pixelgl.KeyDown) { if win.Pressed(pixelgl.KeyLeft) {
if nextState.zIdx <= 0 { nextState.camPos.X -= 4
log.Printf("z index is already at minimum") }
} else {
nextState.redraw = true if win.Pressed(pixelgl.KeyRight) {
nextState.zIdx = nextState.zIdx - 1 nextState.camPos.X += 4
}
if win.Pressed(pixelgl.KeyDown) {
nextState.camPos.Y -= 4
}
if win.Pressed(pixelgl.KeyUp) {
nextState.camPos.Y += 4
}
for i := 0; i <= 6; i++ {
if win.JustPressed(pixelgl.Key1 + pixelgl.Button(i)) {
nextState.zIdx = i
} }
} }
if win.JustPressed(pixelgl.KeyUp) { // Decrease the cell index
if nextState.zIdx >= maps.MaxHeight-1 { if win.JustPressed(pixelgl.KeyMinus) {
log.Printf("z index is already at maximum") if nextState.cellIdx > 0 {
} else { nextState.cellIdx -= 1
nextState.redraw = true
nextState.zIdx = nextState.zIdx + 1
} }
} }
if win.JustPressed(pixelgl.KeyLeft) { // Increase the cell index
if nextState.cellIdx <= 0 { if win.JustPressed(pixelgl.KeyEqual) {
log.Printf("cell index is already at minimum") if nextState.cellIdx < maps.CellSize-1 {
} else { nextState.cellIdx += 1
nextState.redraw = true
nextState.cellIdx = nextState.cellIdx - 1
} }
} }
if win.JustPressed(pixelgl.KeyRight) { // Show details of clicked-on cell in termal
if nextState.cellIdx >= maps.CellSize-1 { if win.JustPressed(pixelgl.MouseButtonLeft) {
log.Printf("cell index is already at maximum") vec := state.cam.Unproject(win.MousePosition())
} else { x, y := vecToCell(vec)
nextState.redraw = true log.Printf("%#v -> %d,%d", vec, x, y)
nextState.cellIdx = nextState.cellIdx + 1 cell := state.env.gameMap.Cells.At(x, y, state.zIdx)
} log.Printf(
"x=%d y=%d z=%d Object0SurfaceArea=%d Object3CenterArea=%d (%s) SquadRelated=%d",
x, y, state.zIdx,
cell.Object0SurfaceArea, cell.Object3CenterArea, state.env.set.Palette[int(cell.Object3CenterArea)],
cell.SquadRelated,
)
log.Printf("CellIdx%d=%d. Full cell data: %#v", state.cellIdx, cell.At(state.cellIdx), cell)
} }
// Zoom in and out with the mouse wheel // Zoom in and out with the mouse wheel
nextState.zoom *= math.Pow(1.2, win.MouseScroll().Y) nextState.zoom *= math.Pow(1.2, win.MouseScroll().Y)
if nextState.zoom != state.zoom { if nextState.zoom != state.zoom {
log.Printf("zoom=%.2f", nextState.zoom) log.Printf("zoom=%.2f", nextState.zoom)
nextState.redraw = true
} }
return &nextState return &nextState

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -248,11 +248,11 @@ We're specifying a viewport on a statically-defined area with a width of 130,
and a length of 100. The viewport is centered on the middle of that area and and a length of 100. The viewport is centered on the middle of that area and
anything outside it is clipped away. anything outside it is clipped away.
## Per-coordinate data ## Per-cell data
All .MAP files are the same size once uncompressed, regardles of width and All .MAP files are the same size once uncompressed, regardles of width and
length parameters, suggesting that they each have fixed-sized records for every length parameters, suggesting that they each have fixed-sized records for every
possible coordinate, regardless. possible cell.
Skipping the header we know about, we have data like this: Skipping the header we know about, we have data like this:
@@ -285,8 +285,10 @@ Skipping the header we know about, we have data like this:
000001F0 38 00 00 00 88 01 00 00 00 00 00 FF 00 00 00 00 8............... 000001F0 38 00 00 00 88 01 00 00 00 00 00 FF 00 00 00 00 8...............
``` ```
It would be very neat if the per-coordinate data started at 0x100 and took 16 It would be very neat if the per-cell data started at 0x100 and took 16 bytes
bytes per coordinate. per coordinate, but to get objects in my map to line up properly with cells in
WH40K_TD.exe, I've had to start parsing these rows at `0x0120` instead. Still
tentative!
Total number of possible coordinates is 100x130x7 = 91,000 = 1,456,000 bytes. Total number of possible coordinates is 100x130x7 = 91,000 = 1,456,000 bytes.
@@ -387,9 +389,55 @@ as expected, so this is a good hint that the data is actually arranged in
`[z][y][x][16]` blocks and that I either have the right offset, or I'm out by a `[z][y][x][16]` blocks and that I either have the right offset, or I'm out by a
well-aligned amount. well-aligned amount.
Trying to constrict to the viewport currently cuts off much of this displayed Investigation has so far suggested the following:
map, suggesting that I've got something wrong in the calculation somewhere. Need
to dig into that more. * `Cell[0]` seems related to doors and canisters. Observed:
* Imperial crate: 0x28
* Door: 0xB8
* `Cell[1]` seems related to special placeables (but not triggers). Bitfield. Observed:
* 0x01: Reactor
* 0x20: Door or door lock?
* 0x40: Animated object
* `Cell[2]` hasn't been seen with a value > 0 yet
* `Cell[3]` Object 0 (Surface) Area (Sets/*.set lookup)
* `Cell[4]` Unsure at present, but it varies between a narrow range of values.
I've seen 128 - 147. Broadly matches the terrain layout.
* `Cell[5]` Object 1 (Left) Area (Sets/*.set lookup) **ASSUMED**.
* It's in the right place, and there seems to be correspondence, but not as
neatly as the other 3 columns. Often off-by-1
* `Cell[6]` Wide range of values, 0 - 161 observed. Seems to have identity with
some blood splatters, etc
* `Cell[7]` Object 2 (Right) Area (Sets/*.set lookup)
* `Cell[8]` Wide range of values, 0 - 159 observed.
* `Cell[9]` Object 3 (Center) Area (Sets/*.set lookup)
* `Cell[10]` Varies from 0 - 248. Unclear what for, broadly follows terrain
* `Cell[11]` all 255?
* `Cell[12]` all 0?
* `Cell[13]` all 0?
* `Cell[14]` all 0?
* `Cell[15]` shows squad positions, MP start positions, etc, as 0x04
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`
has `Area 67` and `Sprite 1`. In `Sets/map01.set`, entry 67, zero-indexed and
ignoring non-palette data, is an altar.
The data of that cell is:
```
maps.Cell{
0x18, 0x0, 0x0, 0x2, 0x99, 0x1, 0x0, 0x0,
0x0, 0x43, 0x81, 0xff, 0x0, 0x0, 0x0, 0x0
}
```
So `CellIdx == 9` points to the center object's Area, looked up in the set file!
![Pinning down cell index 9](img/chapter01_cell_index_9.png
I still see weird artifacts on middle Z-layers making me think I'm off the
stride or perhaps there's a data block partway through or something. Or maybe
the data's just born that way.
## Trailer ## Trailer
@@ -459,4 +507,4 @@ Around 001841A0: mission objectives!
``` ```
Since all the files are exactly the same length uncompressed, I'm going to 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. assume these are all a fixed number of fixed-size records when looking into it.

View File

@@ -26,8 +26,8 @@ const (
CellSize = 16 // seems to be CellSize = 16 // seems to be
cellDataOffset = 0x100 // tentatively cellDataOffset = 0x120 // tentatively
cellDataSize = MaxHeight * MaxLength * MaxWidth * CellSize cellCount = MaxHeight * MaxLength * MaxWidth
) )
type Header struct { type Header struct {
@@ -59,21 +59,79 @@ func (h Header) Height() int {
return MaxHeight return MaxHeight
} }
type Cell []byte // FIXME: need to deconstruct this into the various fields func (h Header) MapSetFilename() string {
idx := bytes.IndexByte(h.SetName[:], 0)
if idx < 0 {
idx = 8 // all 8 bytes are used
}
return string(h.SetName[0:idx:idx]) + ".set"
}
type Cell struct {
DoorAndCanisterRelated byte
DoorLockAndReactorRelated byte
Unknown2 byte
Object0SurfaceArea byte
Unknown4 byte
Object1LeftArea byte
Unknown6 byte
Object2RightArea byte
Unknown8 byte
Object3CenterArea byte
Unknown10 byte
Unknown11 byte
Unknown12 byte
Unknown13 byte
Unknown14 byte
SquadRelated byte
}
func (c *Cell) At(n int) byte {
switch n {
case 0:
return c.DoorAndCanisterRelated
case 1:
return c.DoorLockAndReactorRelated
case 2:
return c.Unknown2
case 3:
return c.Object0SurfaceArea
case 4:
return c.Unknown4
case 5:
return c.Object1LeftArea
case 6:
return c.Unknown6
case 7:
return c.Object2RightArea
case 8:
return c.Unknown8
case 9:
return c.Object3CenterArea
case 10:
return c.Unknown10
case 11:
return c.Unknown11
case 12:
return c.Unknown12
case 13:
return c.Unknown13
case 14:
return c.Unknown14
case 15:
return c.SquadRelated
}
return 0
}
// Cells is always a fixed size; use At to get a cell according to x,y,z // Cells is always a fixed size; use At to get a cell according to x,y,z
type Cells []byte type Cells []Cell
// FIXME: Ordering may be incorrect? I assume z,y,x for now... // FIXME: Ordering may be incorrect? I assume z,y,x for now...
func (c Cells) At(x, y, z int) Cell { func (c Cells) At(x, y, z int) Cell {
start := return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x]
(z * MaxLength * MaxWidth * CellSize) +
(y * MaxWidth * CellSize) +
(x * CellSize)
end := start + CellSize
return Cell(c[start:end:end])
} }
func (h Header) Check() []error { func (h Header) Check() []error {
@@ -194,9 +252,9 @@ func loadMapFile(filename string) (*GameMap, error) {
return nil, err return nil, err
} }
out.Cells = make(Cells, cellDataSize) out.Cells = make(Cells, cellCount)
if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil { if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil {
return nil, fmt.Errorf("Error parsing %s: %v", filename, err) return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err)
} }
return &out, nil return &out, nil