324 lines
8.7 KiB
Go
324 lines
8.7 KiB
Go
// Reference dumper: decode a Diablo II format file with the community Go
|
|
// implementations and print a canonical JSON summary, so this project's
|
|
// TypeScript decoders can be diffed against an independent implementation.
|
|
//
|
|
// Usage: go run main.go <dc6|dt1|ds1> <file>
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
|
|
dc6pkg "github.com/OpenDiablo2/dc6/pkg"
|
|
pl2pkg "github.com/OpenDiablo2/pl2/pkg"
|
|
ds1pkg "github.com/OpenDiablo2/ds1/pkg"
|
|
dt1pkg "github.com/OpenDiablo2/dt1/pkg"
|
|
)
|
|
|
|
// ints converts a byte slice into a JSON-friendly array (Go would otherwise
|
|
// marshal it as base64).
|
|
func ints(data []byte) []int {
|
|
out := make([]int, len(data))
|
|
for i, value := range data {
|
|
out[i] = int(value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 3 {
|
|
fmt.Fprintln(os.Stderr, "usage: main <dc6|dt1|ds1> <file>")
|
|
os.Exit(2)
|
|
}
|
|
data, err := os.ReadFile(os.Args[2])
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
var out any
|
|
switch os.Args[1] {
|
|
case "dc6":
|
|
out, err = dumpDc6(data)
|
|
case "dt1":
|
|
out, err = dumpDt1(data)
|
|
case "ds1":
|
|
out, err = dumpDs1(data)
|
|
case "pl2":
|
|
out, err = dumpPl2(data)
|
|
default:
|
|
err = fmt.Errorf("unknown format %q", os.Args[1])
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
encoded, err := json.MarshalIndent(out, "", " ")
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println(string(encoded))
|
|
}
|
|
|
|
// dumpDc6 reports the header plus every frame's geometry and index grid.
|
|
func dumpDc6(data []byte) (any, error) {
|
|
sheet, err := dc6pkg.FromBytes(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
frames := []any{}
|
|
for _, direction := range sheet.Directions {
|
|
perDirection := []any{}
|
|
for _, frame := range direction.Frames {
|
|
perDirection = append(perDirection, map[string]any{
|
|
"width": int(frame.Width),
|
|
"height": int(frame.Height),
|
|
"offsetX": int(frame.OffsetX),
|
|
"offsetY": int(frame.OffsetY),
|
|
"indices": ints(frame.IndexData),
|
|
})
|
|
}
|
|
frames = append(frames, perDirection)
|
|
}
|
|
perDirection := 0
|
|
if len(sheet.Directions) > 0 {
|
|
perDirection = len(sheet.Directions[0].Frames)
|
|
}
|
|
return map[string]any{
|
|
"format": "dc6",
|
|
"directions": len(sheet.Directions),
|
|
"framesPerDirection": perDirection,
|
|
"frames": frames,
|
|
}, nil
|
|
}
|
|
|
|
// dumpDt1 reports every tile's identity fields, sub-tile flags and block pixels.
|
|
func dumpDt1(data []byte) (any, error) {
|
|
library, err := dt1pkg.FromBytes(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tiles := []any{}
|
|
for _, tile := range library.Tiles {
|
|
flags := []any{}
|
|
for _, flag := range tile.SubTileFlags {
|
|
var raw byte
|
|
if flag.BlockWalk {
|
|
raw |= 1
|
|
}
|
|
if flag.BlockLOS {
|
|
raw |= 2
|
|
}
|
|
if flag.BlockJump {
|
|
raw |= 4
|
|
}
|
|
if flag.BlockPlayerWalk {
|
|
raw |= 8
|
|
}
|
|
if flag.BlockLight {
|
|
raw |= 32
|
|
}
|
|
flags = append(flags, int(raw))
|
|
}
|
|
blocks := []any{}
|
|
for _, block := range tile.Blocks {
|
|
blocks = append(blocks, map[string]any{
|
|
"x": int(block.X),
|
|
"y": int(block.Y),
|
|
"gridX": int(block.GridX),
|
|
"gridY": int(block.GridY),
|
|
"format": int(block.Format),
|
|
"pixels": ints(block.PixelData),
|
|
})
|
|
}
|
|
material := 0
|
|
if tile.MaterialFlags.Other {
|
|
material |= 0x0001
|
|
}
|
|
if tile.MaterialFlags.Water {
|
|
material |= 0x0002
|
|
}
|
|
if tile.MaterialFlags.WoodObject {
|
|
material |= 0x0004
|
|
}
|
|
if tile.MaterialFlags.InsideStone {
|
|
material |= 0x0008
|
|
}
|
|
if tile.MaterialFlags.OutsideStone {
|
|
material |= 0x0010
|
|
}
|
|
if tile.MaterialFlags.Dirt {
|
|
material |= 0x0020
|
|
}
|
|
if tile.MaterialFlags.Sand {
|
|
material |= 0x0040
|
|
}
|
|
if tile.MaterialFlags.Wood {
|
|
material |= 0x0080
|
|
}
|
|
if tile.MaterialFlags.Lava {
|
|
material |= 0x0100
|
|
}
|
|
if tile.MaterialFlags.Snow {
|
|
material |= 0x0400
|
|
}
|
|
tiles = append(tiles, map[string]any{
|
|
"direction": int(tile.Direction),
|
|
"height": int(tile.Height),
|
|
"width": int(tile.Width),
|
|
"type": int(tile.Type),
|
|
"style": int(tile.Style),
|
|
"sequence": int(tile.Sequence),
|
|
"materialFlags": material,
|
|
"subTileFlags": flags,
|
|
"blocks": blocks,
|
|
})
|
|
}
|
|
return map[string]any{"format": "dt1", "tiles": tiles}, nil
|
|
}
|
|
|
|
// dumpDs1 reports the map header, every cell's layers and the object list.
|
|
func dumpDs1(data []byte) (any, error) {
|
|
level, err := ds1pkg.FromBytes(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows := []any{}
|
|
for y := 0; y < int(level.Height); y++ {
|
|
row := []any{}
|
|
for x := 0; x < int(level.Width); x++ {
|
|
cell := level.Tiles[y][x]
|
|
walls := []any{}
|
|
for _, wall := range cell.Walls {
|
|
walls = append(walls, map[string]any{
|
|
"prop1": int(wall.Prop1),
|
|
"sequence": int(wall.Sequence),
|
|
"style": int(wall.Style),
|
|
"type": int(wall.Type),
|
|
"unknown1": int(wall.Unknown1),
|
|
"unknown2": int(wall.Unknown2),
|
|
"hidden": wall.Hidden,
|
|
})
|
|
}
|
|
floors := []any{}
|
|
for _, floor := range cell.Floors {
|
|
floors = append(floors, map[string]any{
|
|
"prop1": int(floor.Prop1),
|
|
"sequence": int(floor.Sequence),
|
|
"style": int(floor.Style),
|
|
"unknown1": int(floor.Unknown1),
|
|
"unknown2": int(floor.Unknown2),
|
|
"hidden": floor.Hidden,
|
|
})
|
|
}
|
|
shadows := []any{}
|
|
for _, shadow := range cell.Shadows {
|
|
shadows = append(shadows, map[string]any{
|
|
"prop1": int(shadow.Prop1),
|
|
"sequence": int(shadow.Sequence),
|
|
"style": int(shadow.Style),
|
|
"unknown1": int(shadow.Unknown1),
|
|
"unknown2": int(shadow.Unknown2),
|
|
"hidden": shadow.Hidden,
|
|
})
|
|
}
|
|
row = append(row, map[string]any{"walls": walls, "floors": floors, "shadows": shadows})
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
objects := []any{}
|
|
for _, object := range level.Objects {
|
|
objects = append(objects, map[string]any{
|
|
"type": int(object.Type), "id": int(object.ID),
|
|
"x": int(object.X), "y": int(object.Y), "flags": int(object.Flags),
|
|
})
|
|
}
|
|
return map[string]any{
|
|
"format": "ds1",
|
|
"version": int(level.Version),
|
|
"width": int(level.Width),
|
|
"height": int(level.Height),
|
|
"act": int(level.Act),
|
|
"wallLayers": int(level.NumberOfWalls),
|
|
"floorLayers": int(level.NumberOfFloors),
|
|
"cells": rows,
|
|
"objects": objects,
|
|
}, nil
|
|
}
|
|
|
|
// fnv1a is a small digest so whole tables can be compared without shipping
|
|
// 442 KB of numbers through JSON.
|
|
func fnv1a(data []byte) uint32 {
|
|
var hash uint32 = 2166136261
|
|
for _, b := range data {
|
|
hash ^= uint32(b)
|
|
hash *= 16777619
|
|
}
|
|
return hash
|
|
}
|
|
|
|
func digestTables(tables [][]byte) []uint32 {
|
|
for _, table := range tables {
|
|
_ = table
|
|
}
|
|
out := make([]uint32, 0, len(tables))
|
|
for _, table := range tables {
|
|
out = append(out, fnv1a(table))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// dumpPl2 reports the base palette, the text palette and a per-group digest of
|
|
// every transform table, so a layout disagreement shows up as a digest mismatch.
|
|
func dumpPl2(data []byte) (any, error) {
|
|
palette, err := pl2pkg.FromBytes(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base := []int{}
|
|
for _, c := range palette.BasePalette {
|
|
r, g, b, _ := c.RGBA()
|
|
base = append(base, int(uint8(r)), int(uint8(g)), int(uint8(b)))
|
|
}
|
|
text := []int{}
|
|
for _, c := range palette.TextColors {
|
|
r, g, b, _ := c.RGBA()
|
|
text = append(text, int(uint8(r)), int(uint8(g)), int(uint8(b)))
|
|
}
|
|
asBytes := func(tables []pl2pkg.Transform) [][]byte {
|
|
out := make([][]byte, 0, len(tables))
|
|
for i := range tables {
|
|
out = append(out, tables[i][:])
|
|
}
|
|
return out
|
|
}
|
|
alpha := [][]byte{}
|
|
for i := range palette.AlphaBlend {
|
|
alpha = append(alpha, asBytes(palette.AlphaBlend[i])...)
|
|
}
|
|
return map[string]any{
|
|
"format": "pl2",
|
|
"size": len(data),
|
|
"base": base,
|
|
"text": text,
|
|
"groups": map[string]any{
|
|
"lightLevels": digestTables(asBytes(palette.LightLevelVariations)),
|
|
"inverseColors": digestTables(asBytes(palette.InvColorVariations)),
|
|
"selectedUnitShift": digestTables(asBytes([]pl2pkg.Transform{palette.SelectedUnitShift})),
|
|
"alphaBlend": digestTables(alpha),
|
|
"additiveBlend": digestTables(asBytes(palette.AdditiveBlend)),
|
|
"multiplyBlend": digestTables(asBytes(palette.MultiplicativeBlend)),
|
|
"hueVariations": digestTables(asBytes(palette.HueVariations)),
|
|
"redTones": digestTables(asBytes([]pl2pkg.Transform{palette.RedTones})),
|
|
"greenTones": digestTables(asBytes([]pl2pkg.Transform{palette.GreenTones})),
|
|
"blueTones": digestTables(asBytes([]pl2pkg.Transform{palette.BlueTones})),
|
|
"unknownVariations": digestTables(asBytes(palette.UnknownVariations)),
|
|
"maxComponentBlend": digestTables(asBytes(palette.MaxComponentBlend)),
|
|
"darkenedShift": digestTables(asBytes([]pl2pkg.Transform{palette.DarkenedColorShift})),
|
|
"textShifts": digestTables(asBytes(palette.TextColorShifts)),
|
|
},
|
|
}, nil
|
|
}
|