Files
questshock/engine-debug-tools/dump2png/main.go
T
ml 9d0a0d5798
build / build (push) Successful in 2m52s
Add engine-debug-tools and fix critter animation rendering bugs
- engine-debug-tools/: a debug dump hotkey + dump2png to inspect
  decoded critter frames independent of the renderer (see root
  README.md's "Debugging" section).
- engine-patches/04,05: fix critter sprite scale and an OpenGL
  texture cache collision, found using that tooling.
- mdlp: fix encodeFrames RLE-diffing upscaled frames against a
  previous-frame reference the real RSD8 decoder never implements
  (verified against Night Dive's original Mac source down to the
  68K assembly) - was rendering as scattered static in-game.
- Upscale pipeline: better color quantization (dithering, pure-ramp
  exclusion) and per-resource model selection.
2026-10-18 06:59:47 +02:00

106 lines
2.8 KiB
Go

// dump2png converts the raw indexed-pixel frame dumps written by the
// engine's debug dump hotkey (see ../engine-patches/ and the root
// README.md's "Debugging" section) into viewable PNGs, using the real game
// palette - see that same README section for the dump file format.
package main
import (
"encoding/binary"
"fmt"
"image"
"image/color"
"image/png"
"os"
"path/filepath"
"sort"
"strings"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/world"
)
func main() {
if len(os.Args) != 4 {
fmt.Fprintln(os.Stderr, "usage: dump2png <game-data-dir> <dumps-dir> <out-dir>")
os.Exit(1)
}
dataDir, inDir, outDir := os.Args[1], os.Args[2], os.Args[3]
entries, err := os.ReadDir(dataDir)
if err != nil {
panic(err)
}
merged := &world.ManifestEntry{Origin: []string{dataDir}, ID: dataDir}
for _, e := range entries {
if e.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(e.Name()))
if ext != ".res" && ext != ".dat" {
continue
}
fe, err := world.NewManifestEntryFrom([]string{filepath.Join(dataDir, e.Name())})
if err != nil {
continue
}
merged.Resources = append(merged.Resources, fe.Resources...)
}
sort.Slice(merged.Resources, func(i, j int) bool { return merged.Resources[i].ID < merged.Resources[j].ID })
mod := world.NewMod(func([]resource.ID, []resource.ID) {}, func() {})
if err := mod.World().InsertEntry(0, merged); err != nil {
panic(err)
}
pal, err := bitmap.NewPaletteCache(mod).Palette(resource.KeyOf(0x02BC, resource.LangDefault, 0))
if err != nil {
panic(err)
}
colors := pal.ColorPalette(false)
if err := os.MkdirAll(outDir, 0o755); err != nil {
panic(err)
}
files, err := os.ReadDir(inDir)
if err != nil {
panic(err)
}
for _, fi := range files {
if !strings.HasSuffix(fi.Name(), ".raw") {
continue
}
raw, err := os.ReadFile(filepath.Join(inDir, fi.Name()))
if err != nil {
fmt.Println("skip", fi.Name(), err)
continue
}
if len(raw) < 4 {
continue
}
w := int(binary.LittleEndian.Uint16(raw[0:2]))
h := int(binary.LittleEndian.Uint16(raw[2:4]))
pixels := raw[4:]
if len(pixels) < w*h {
fmt.Println("truncated", fi.Name())
continue
}
img := image.NewRGBA(image.Rect(0, 0, w, h))
for i := 0; i < w*h; i++ {
r, g, b, _ := colors[pixels[i]].RGBA()
img.Set(i%w, i/w, color.RGBA{R: byte(r >> 8), G: byte(g >> 8), B: byte(b >> 8), A: 0xff})
}
outPath := filepath.Join(outDir, strings.TrimSuffix(fi.Name(), ".raw")+".png")
of, err := os.Create(outPath)
if err != nil {
fmt.Println("create failed", outPath, err)
continue
}
err = png.Encode(of, img)
of.Close()
if err != nil {
fmt.Println("encode failed", outPath, err)
continue
}
fmt.Println("wrote", outPath, w, h)
}
}