From 1b10fd78060e160168d9ddadfd385b7263006a90 Mon Sep 17 00:00:00 2001 From: Luciano Ciccariello Date: Tue, 29 Oct 2024 08:38:05 +0000 Subject: [PATCH] Asset tool refactor (#1844) `sotn-assets stage extract` and `sotn-assets stage build` are gone in favour of `config/assets.$(VERSION).yaml`. `sotn-asset stage info` is now replaced with `sotn-stage info` to retrieve expanded metadata on how to use the tool for new overlays and reduce the burden of hunting for data. There are tons of change, too many to describe. In short the tool is much simpler than before and data is decoupled from each other. Each data type is defined as a handler. A handler has `Extract` to create the files in `assets/`, a `Build` to convert `assets/` files into embeddedable code in `src/`, and `Info` to get stage metadata. Please refer to the commit list for a breakdown of the changes done. --- Makefile.psx.mk | 35 +- config/assets.hd.yaml | 8 + config/assets.us.yaml | 43 ++ go.work.sum | 3 +- tools/sotn-assets/asset_config.go | 43 +- tools/sotn-assets/assets/assets.go | 42 +- tools/sotn-assets/assets/cutscene/handler.go | 8 +- tools/sotn-assets/assets/graphics/graphics.go | 106 ++++ tools/sotn-assets/assets/layer/handler.go | 190 ++++++ .../{build.go => assets/layer/layer.go} | 578 +++++++++--------- .../{ => assets/layer}/tile_def.go | 50 +- .../{ => assets/layer}/tile_map.go | 18 +- tools/sotn-assets/assets/layout/handler.go | 101 +++ tools/sotn-assets/assets/layout/layout.go | 305 +++++++++ tools/sotn-assets/assets/rooms/handler.go | 36 +- tools/sotn-assets/assets/skip/handler.go | 21 + .../sotn-assets/assets/spritebanks/handler.go | 38 +- .../assets/spritebanks/spritebanks.go | 6 +- tools/sotn-assets/assets/spriteset/handler.go | 10 +- tools/sotn-assets/go.mod | 11 +- tools/sotn-assets/go.sum | 9 + tools/sotn-assets/graphics.go | 105 ---- tools/sotn-assets/info.go | 74 +++ tools/sotn-assets/info_test.go | 78 +++ tools/sotn-assets/layer.go | 133 ---- tools/sotn-assets/layout.go | 213 ------- tools/sotn-assets/main.go | 379 +----------- tools/sotn-assets/paths.go | 30 - tools/sotn-assets/sotn/enum.go | 72 +++ tools/sotn-assets/sotn/enum_test.go | 33 + tools/sotn-assets/sotn/stage.go | 33 + tools/sotn-assets/{ => util}/utils.go | 12 +- 32 files changed, 1559 insertions(+), 1264 deletions(-) create mode 100644 tools/sotn-assets/assets/graphics/graphics.go create mode 100644 tools/sotn-assets/assets/layer/handler.go rename tools/sotn-assets/{build.go => assets/layer/layer.go} (56%) rename tools/sotn-assets/{ => assets/layer}/tile_def.go (57%) rename tools/sotn-assets/{ => assets/layer}/tile_map.go (68%) create mode 100644 tools/sotn-assets/assets/layout/handler.go create mode 100644 tools/sotn-assets/assets/layout/layout.go create mode 100644 tools/sotn-assets/assets/skip/handler.go delete mode 100644 tools/sotn-assets/graphics.go create mode 100644 tools/sotn-assets/info.go create mode 100644 tools/sotn-assets/info_test.go delete mode 100644 tools/sotn-assets/layer.go delete mode 100644 tools/sotn-assets/layout.go delete mode 100644 tools/sotn-assets/paths.go create mode 100644 tools/sotn-assets/sotn/enum.go create mode 100644 tools/sotn-assets/sotn/enum_test.go create mode 100644 tools/sotn-assets/sotn/stage.go rename tools/sotn-assets/{ => util}/utils.go (77%) diff --git a/Makefile.psx.mk b/Makefile.psx.mk index 1dbf45f49..47a1af21c 100644 --- a/Makefile.psx.mk +++ b/Makefile.psx.mk @@ -21,8 +21,8 @@ extract_us: $(addprefix $(BUILD_DIR)/,$(addsuffix .ld,$(PSX_US_TARGETS))) make extract_assets make build_assets extract_hd: $(addprefix $(BUILD_DIR)/,$(addsuffix .ld,$(PSX_HD_TARGETS))) - make extract_assets_hd - make build_assets_hd + make extract_assets + make build_assets extract_disk_us: extract_disk_psxus extract_disk_hd: extract_disk_pspeu @@ -86,36 +86,9 @@ $(BUILD_DIR)/$(SRC_DIR)/main/psxsdk/libgpu/sys.c.o: $(SRC_DIR)/main/psxsdk/libgp extract_assets: $(SOTNASSETS) cd tools/sotn-assets; $(GO) install - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/ST/CEN/CEN.BIN -o assets/st/cen - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/ST/DRE/DRE.BIN -o assets/st/dre - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/ST/NO3/NO3.BIN -o assets/st/no3 - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/ST/NP3/NP3.BIN -o assets/st/np3 - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/ST/NZ0/NZ0.BIN -o assets/st/nz0 - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/ST/ST0/ST0.BIN -o assets/st/st0 - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/ST/WRP/WRP.BIN -o assets/st/wrp - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/ST/RWRP/RWRP.BIN -o assets/st/rwrp - $(SOTNASSETS) stage extract -stage_ovl disks/$(VERSION)/BOSS/MAR/MAR.BIN -o assets/boss/mar - $(SOTNASSETS) config extract config/assets.$(VERSION).yaml -extract_assets_hd: $(SOTNASSETS) - cd tools/sotn-assets; $(GO) install - $(SOTNASSETS) stage extract -stage_ovl disks/pspeu/PSP_GAME/USRDIR/res/ps/hdbin/cen.bin -o assets/st/cen - $(SOTNASSETS) stage extract -stage_ovl disks/pspeu/PSP_GAME/USRDIR/res/ps/hdbin/wrp.bin -o assets/st/wrp - $(SOTNASSETS) config extract config/assets.$(VERSION).yaml + $(SOTNASSETS) extract config/assets.$(VERSION).yaml build_assets: $(SOTNASSETS) - $(SOTNASSETS) stage build_all -i assets/st/cen -o src/st/cen/ - $(SOTNASSETS) stage build_all -i assets/st/dre -o src/st/dre/ - $(SOTNASSETS) stage build_all -i assets/st/no3 -o src/st/no3/ - $(SOTNASSETS) stage build_all -i assets/st/np3 -o src/st/np3/ - $(SOTNASSETS) stage build_all -i assets/st/nz0 -o src/st/nz0/ - $(SOTNASSETS) stage build_all -i assets/st/st0 -o src/st/st0/ - $(SOTNASSETS) stage build_all -i assets/st/wrp -o src/st/wrp/ - $(SOTNASSETS) stage build_all -i assets/st/rwrp -o src/st/rwrp/ - $(SOTNASSETS) stage build_all -i assets/boss/mar -o src/boss/mar/ - $(SOTNASSETS) config build config/assets.$(VERSION).yaml -build_assets_hd: $(SOTNASSETS) - $(SOTNASSETS) stage build_all -i assets/st/cen -o src/st/cen/ - $(SOTNASSETS) stage build_all -i assets/st/wrp -o src/st/wrp/ - $(SOTNASSETS) config build config/assets.$(VERSION).yaml + $(SOTNASSETS) build config/assets.$(VERSION).yaml $(BUILD_DIR)/assets/dra/memcard_%.png.o: assets/dra/memcard_%.png mkdir -p $(dir $@) diff --git a/config/assets.hd.yaml b/config/assets.hd.yaml index 2440780f7..9a6d31afc 100644 --- a/config/assets.hd.yaml +++ b/config/assets.hd.yaml @@ -8,6 +8,10 @@ files: assets: - [0x40, sprite_banks, sprite_banks] - [0xA0, skip] + - [0xDC, layers, layers] + - [0x134, skip] + - [0x1EC, layout, entity_layouts] + - [0x394, skip] - [0x1308, rooms, rooms] - [0x1334, skip] - target: disks/pspeu/PSP_GAME/USRDIR/res/ps/hdbin/wrp.bin @@ -19,5 +23,9 @@ files: assets: - [0x40, sprite_banks, sprite_banks] - [0xA0, skip] + - [0xB8, layers, layers] + - [0x1B8, skip] + - [0x23C, layout, entity_layouts] + - [0x3E4, skip] - [0x11B0, rooms, rooms] - [0x122C, skip] diff --git a/config/assets.us.yaml b/config/assets.us.yaml index c363dba80..b8c5587d5 100644 --- a/config/assets.us.yaml +++ b/config/assets.us.yaml @@ -8,6 +8,10 @@ files: assets: - [0x40, sprite_banks, sprite_banks] - [0xA0, skip] + - [0xDC, layers, layers] + - [0x134, skip] + - [0x1EC, layout, entity_layouts] + - [0x394, skip] - [0x12D4, rooms, rooms] - [0x1300, skip] - [0x13F0, cutscene, cutscene_data] @@ -21,6 +25,10 @@ files: assets: - [0x40, sprite_banks, sprite_banks] - [0xA0, skip] + - [0xE8, layers, layers] + - [0x128, skip] + - [0x220, layout, entity_layouts] + - [0x3C8, skip] - [0x1498, rooms, rooms] - [0x14AC, skip] - [0x16C8, cutscene, cutscene_data] @@ -34,6 +42,7 @@ files: assets: - [0x40, sprite_banks, sprite_banks] - [0xA0, skip] + # TODO broken - [0x1130, rooms, rooms] - [0x11D4, skip] - target: disks/us/ST/NO3/NO3.BIN @@ -45,6 +54,10 @@ files: assets: - [0x2C, sprite_banks, sprite_banks] - [0x8C, skip] + - [0x1C4, layers, layers] + - [0x5A4, skip] + - [0x77C, layout, entity_layouts] + - [0x924, skip] - [0x3CC4, rooms, rooms] - [0x3DC8, skip] - [0x4CE0, cutscene, cutscene_data] @@ -58,6 +71,10 @@ files: assets: - [0x2C, sprite_banks, sprite_banks] - [0x8C, skip] + - [0x1D0, layers, layers] + - [0x558, skip] + - [0x728, layout, entity_layouts] + - [0x8D0, skip] - [0x3A7C, rooms, rooms] - [0x3B68, skip] - target: disks/us/ST/NZ0/NZ0.BIN @@ -69,6 +86,10 @@ files: assets: - [0x2C, sprite_banks, sprite_banks] - [0x8C, skip] + - [0x164, layers, layers] + - [0x47C, skip] + - [0x8EC, layout, entity_layouts] + - [0xA94, skip] - [0x272C, rooms, rooms] - [0x2830, skip] - [0x3B0C, cutscene, cutscene_data] @@ -82,6 +103,10 @@ files: assets: - [0x40, sprite_banks, sprite_banks] - [0xA0, skip] + - [0x124, layers, layers] + - [0x1A4, skip] + - [0x314, layout, entity_layouts] + - [0x4BC, skip] - [0x2060, rooms, rooms] - [0x2084, skip] - [0x29D8, cutscene, cutscene_data] @@ -95,6 +120,10 @@ files: assets: - [0x40, sprite_banks, sprite_banks] - [0xA0, skip] + - [0xB8, layers, layers] + - [0x1B8, skip] + - [0x23C, layout, entity_layouts] + - [0x3E4, skip] - [0x11AC, rooms, rooms] - [0x1228, skip] - target: disks/us/ST/RWRP/RWRP.BIN @@ -106,6 +135,10 @@ files: assets: - [0x40, sprite_banks, sprite_banks] - [0xA0, skip] + - [0xB8, layers, layers] + - [0x1B8, skip] + - [0x23C, layout, entity_layouts] + - [0x3E4, skip] - [0x11AC, rooms, rooms] - [0x1228, skip] - target: disks/us/BOSS/MAR/MAR.BIN @@ -117,6 +150,10 @@ files: assets: - [0x2C, sprite_banks, sprite_banks] - [0x8C, skip] + - [0xCC, layers, layers] + - [0xF4, skip] + - [0x168, layout, entity_layouts] + - [0x310, skip] - [0x12EC, rooms, rooms] - [0x1308, skip] - [0x1424, cutscene, cutscene_data] @@ -130,6 +167,12 @@ files: assets: - [0x2C, sprite_banks, sprite_banks] - [0x8C, skip] + - [0xE0, layers, layers] + - [0x108, skip] + - [0x1EC, layout, entity_layouts] + - [0x394, skip] + - [0x126C, rooms, rooms] + - [0x1288, skip] - target: disks/us/BIN/WEAPON0.BIN asset_path: assets/weapon src_path: src/weapon diff --git a/go.work.sum b/go.work.sum index 5ab138f8a..93e5f73a8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1 +1,2 @@ -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= diff --git a/tools/sotn-assets/asset_config.go b/tools/sotn-assets/asset_config.go index 49dc3a8a5..48d8a9c84 100644 --- a/tools/sotn-assets/asset_config.go +++ b/tools/sotn-assets/asset_config.go @@ -4,7 +4,10 @@ import ( "fmt" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/cutscene" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/layer" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/layout" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/rooms" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/skip" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/spritebanks" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/spriteset" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" @@ -31,19 +34,21 @@ type assetConfig struct { Files []assetFileEntry `yaml:"files"` } -var extractHandlers = map[string]func(assets.ExtractEntry) error{ - "cutscene": cutscene.Handler.Extract, - "rooms": rooms.Handler.Extract, - "sprite_banks": spritebanks.Handler.Extract, - "spriteset": spriteset.Handler.Extract, -} - -var buildHandlers = map[string]func(assets.BuildEntry) error{ - "cutscene": cutscene.Handler.Build, - "rooms": rooms.Handler.Build, - "sprite_banks": spritebanks.Handler.Build, - "spriteset": spriteset.Handler.Build, -} +var handlers = func() map[string]assets.Handler { + m := make(map[string]assets.Handler) + for _, handler := range []assets.Handler{ + cutscene.Handler, + layer.Handler, + layout.Handler, + rooms.Handler, + skip.Handler, + spritebanks.Handler, + spriteset.Handler, + } { + m[handler.Name()] = handler + } + return m +}() func parseArgs(entry []string) (offset int64, kind string, args []string, err error) { if len(entry) < 2 { @@ -79,7 +84,7 @@ func readConfig(path string) (*assetConfig, error) { func enqueueExtractAssetEntry( eg *errgroup.Group, - handler func(assets.ExtractEntry) error, + handler assets.Extractor, assetDir string, name string, data []byte, @@ -93,7 +98,7 @@ func enqueueExtractAssetEntry( fmt.Printf("unable to extract asset %q: %v", name, err) } }() - if err := handler(assets.ExtractEntry{ + if err := handler.Extract(assets.ExtractArgs{ Data: data, Start: start, End: end, @@ -132,7 +137,7 @@ func extractAssetFile(file assetFileEntry) error { return fmt.Errorf("offset 0x%X should be smaller than 0x%X, asset %v", off, off2, segment.Assets[i-1]) } if kind != "skip" { - if handler, found := extractHandlers[kind]; found { + if handler, found := handlers[kind]; found { name := strconv.FormatUint(uint64(off), 16) if len(args) > 0 { name = args[0] @@ -155,12 +160,12 @@ func extractAssetFile(file assetFileEntry) error { func enqueueBuildAssetEntry( eg *errgroup.Group, - handler func(assets.BuildEntry) error, + handler assets.Builder, assetDir, sourceDir, name string) { eg.Go(func() error { - err := handler(assets.BuildEntry{ + err := handler.Build(assets.BuildArgs{ AssetDir: assetDir, SrcDir: sourceDir, Name: name, @@ -199,7 +204,7 @@ func buildAssetFile(file assetFileEntry) error { if kind == "skip" { continue } - if handler, found := buildHandlers[kind]; found { + if handler, found := handlers[kind]; found { name := strconv.FormatUint(uint64(off), 16) if len(args) > 0 { name = args[0] diff --git a/tools/sotn-assets/assets/assets.go b/tools/sotn-assets/assets/assets.go index e7fdddbaf..50340097d 100644 --- a/tools/sotn-assets/assets/assets.go +++ b/tools/sotn-assets/assets/assets.go @@ -1,8 +1,11 @@ package assets -import "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" +import ( + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" +) -type ExtractEntry struct { +type ExtractArgs struct { Data []byte Start int End int @@ -11,23 +14,44 @@ type ExtractEntry struct { Args []string RamBase psx.Addr } +type Extractor interface { + Extract(a ExtractArgs) error +} -type BuildEntry struct { +type BuildArgs struct { AssetDir string SrcDir string Name string } - -type Extracter interface { - Extract(e ExtractEntry) error +type Builder interface { + Build(a BuildArgs) error } -type Builder interface { - Build(e BuildEntry) error +type InfoArgs struct { + StageFilePath string + StageData []byte +} +type InfoAssetEntry struct { + DataRange datarange.DataRange + Kind string + Name string +} +type InfoSplatEntry struct { + DataRange datarange.DataRange + Name string + Comment string +} +type InfoResult struct { + AssetEntries []InfoAssetEntry + SplatEntries []InfoSplatEntry +} +type InfoGatherer interface { + Info(a InfoArgs) (InfoResult, error) } type Handler interface { Name() string - Extracter + Extractor Builder + InfoGatherer } diff --git a/tools/sotn-assets/assets/cutscene/handler.go b/tools/sotn-assets/assets/cutscene/handler.go index 89bedc91c..108150b40 100644 --- a/tools/sotn-assets/assets/cutscene/handler.go +++ b/tools/sotn-assets/assets/cutscene/handler.go @@ -20,7 +20,7 @@ var Handler = &handler{} func (h *handler) Name() string { return "cutscene" } -func (h *handler) Extract(e assets.ExtractEntry) error { +func (h *handler) Extract(e assets.ExtractArgs) error { if e.Start == e.End { return fmt.Errorf("a cutscene script cannot be 0 bytes") } @@ -56,7 +56,7 @@ type scriptSrc struct { Script [][]string `yaml:"script"` } -func (h *handler) Build(e assets.BuildEntry) error { +func (h *handler) Build(e assets.BuildArgs) error { inFileName := assetPath(e.AssetDir, e.Name) data, err := os.ReadFile(inFileName) if err != nil { @@ -110,6 +110,10 @@ func (h *handler) Build(e assets.BuildEntry) error { return os.WriteFile(sourcePath(e.SrcDir, e.Name), []byte(sb.String()), 0644) } +func (h *handler) Info(a assets.InfoArgs) (assets.InfoResult, error) { + return assets.InfoResult{}, nil +} + func assetPath(dir, name string) string { if name == "" { name = "cutscene_script" diff --git a/tools/sotn-assets/assets/graphics/graphics.go b/tools/sotn-assets/assets/graphics/graphics.go new file mode 100644 index 000000000..db1f8c97a --- /dev/null +++ b/tools/sotn-assets/assets/graphics/graphics.go @@ -0,0 +1,106 @@ +package graphics + +import ( + "encoding/binary" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/util" + "io" +) + +type GfxKind uint16 + +const ( + GfxBankNone = GfxKind(iota) + GfxBank4bpp + GfxBank8bpp + GfxBank16bpp + GfxBankCompressed +) + +type GfxEntry struct { + X uint16 + Y uint16 + Width uint16 + Height uint16 + Data psx.Addr +} + +type GfxBlock struct { + kind GfxKind + flags uint16 + entries []GfxEntry +} + +type Gfx struct { + blocks []GfxBlock + indices []int +} + +func ReadGraphics(r io.ReadSeeker, off psx.Addr) (Gfx, datarange.DataRange, error) { + if err := off.MoveFile(r, psx.RamStageBegin); err != nil { + return Gfx{}, datarange.DataRange{}, err + } + + // all the offsets are before the array, so it is easy to find where the offsets array ends + var blockOffsets []psx.Addr + for { + var offBank psx.Addr + if err := binary.Read(r, binary.LittleEndian, &offBank); err != nil { + return Gfx{}, datarange.DataRange{}, err + } + if offBank >= off { + break + } + blockOffsets = append(blockOffsets, offBank) + } + + // the order of each GfxBlock must be preserved + pool := map[psx.Addr]int{} + pool[psx.RamNull] = -1 + var blocks []GfxBlock + var ranges []datarange.DataRange + for _, blockOffset := range util.SortUniqueOffsets(blockOffsets) { + if blockOffset == psx.RamNull { // exception for ST0 + continue + } + if err := blockOffset.MoveFile(r, psx.RamStageBegin); err != nil { + return Gfx{}, datarange.DataRange{}, err + } + var block GfxBlock + if err := binary.Read(r, binary.LittleEndian, &block.kind); err != nil { + return Gfx{}, datarange.DataRange{}, err + } + if err := binary.Read(r, binary.LittleEndian, &block.flags); err != nil { + return Gfx{}, datarange.DataRange{}, err + } + + if block.kind == GfxKind(0xFFFF) && block.flags == 0xFFFF { // exception for ST0 + pool[blockOffset] = len(blocks) + blocks = append(blocks, block) + ranges = append(ranges, datarange.FromAddr(blockOffset, 4)) + continue + } + + for { + var entry GfxEntry + if err := binary.Read(r, binary.LittleEndian, &entry); err != nil { + return Gfx{}, datarange.DataRange{}, err + } + if entry.X == 0xFFFF && entry.Y == 0xFFFF { + break + } + block.entries = append(block.entries, entry) + } + pool[blockOffset] = len(blocks) + blocks = append(blocks, block) + ranges = append(ranges, datarange.FromAddr(blockOffset, 4+len(block.entries)*12+4)) + } + + var g Gfx + for _, blockOffset := range blockOffsets { + g.indices = append(g.indices, pool[blockOffset]) + } + + return g, datarange.MergeDataRanges(append(ranges, datarange.FromAddr(off, len(blockOffsets)*4))), nil +} diff --git a/tools/sotn-assets/assets/layer/handler.go b/tools/sotn-assets/assets/layer/handler.go new file mode 100644 index 000000000..86bf985dd --- /dev/null +++ b/tools/sotn-assets/assets/layer/handler.go @@ -0,0 +1,190 @@ +package layer + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/sotn" + "os" + "path" + "path/filepath" +) + +type handler struct{} + +var Handler = &handler{} + +func (h *handler) Name() string { return "layers" } + +func (h *handler) Extract(e assets.ExtractArgs) error { + r := bytes.NewReader(e.Data) + header, err := sotn.ReadStageHeader(r) + if err != nil { + return err + } + l, _, err := readLayers(r, header.Layers) + if err != nil { + return fmt.Errorf("unable to read layers: %w", err) + } + tileMaps, tileMapsRange, err := readAllTileMaps(r, l) + if err != nil { + return fmt.Errorf("unable to gather all the tile maps: %w", err) + } + tileDefs, tileDefsRange, err := readAllTiledefs(r, l) + if err != nil { + return fmt.Errorf("unable to gather all the tile defs: %w", err) + } + + // check for unused tile defs (CEN has one) + for tileMapsRange.End() < tileDefsRange.Begin() { + offset := tileDefsRange.Begin().Sum(-0x10) + unusedTileDef, unusedTileDefRange, err := readTiledef(r, offset) + if err != nil { + return fmt.Errorf("there is a gap between tileMaps and tileDefs: %w", err) + } + tileDefs[offset] = unusedTileDef + tileDefsRange = datarange.MergeDataRanges([]datarange.DataRange{tileDefsRange, unusedTileDefRange}) + } + + outFileName := path.Join(e.AssetDir, "layers.json") + dir := filepath.Dir(outFileName) + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Printf("failed to create directory %s: %v\n", dir, err) + return err + } + + content, err := json.MarshalIndent(l, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(outFileName, content, 0644); err != nil { + return fmt.Errorf("unable to create layers file: %w", err) + } + + for offset, data := range tileMaps { + fileName := path.Join(e.AssetDir, tilemapFileName(offset)) + if err := os.WriteFile(fileName, data, 0644); err != nil { + return fmt.Errorf("unable to create %q: %w", fileName, err) + } + } + + for offset, tileDefsData := range tileDefs { + defs := tileDefPaths{ + Tiles: tiledefIndicesFileName(offset), + Pages: tiledefPagesFileName(offset), + Cluts: tiledefClutsFileName(offset), + Collisions: tiledefCollisionsFileName(offset), + } + if err := os.WriteFile(path.Join(e.AssetDir, defs.Tiles), tileDefsData.Tiles, 0644); err != nil { + return fmt.Errorf("unable to create %q: %w", defs.Tiles, err) + } + if err := os.WriteFile(path.Join(e.AssetDir, defs.Pages), tileDefsData.Pages, 0644); err != nil { + return fmt.Errorf("unable to create %q: %w", defs.Pages, err) + } + if err := os.WriteFile(path.Join(e.AssetDir, defs.Cluts), tileDefsData.Cluts, 0644); err != nil { + return fmt.Errorf("unable to create %q: %w", defs.Cluts, err) + } + if err := os.WriteFile(path.Join(e.AssetDir, defs.Collisions), tileDefsData.Cols, 0644); err != nil { + return fmt.Errorf("unable to create %q: %w", defs.Collisions, err) + } + + content, err = json.MarshalIndent(defs, "", " ") + if err != nil { + return err + } + fileName := path.Join(e.AssetDir, tiledefFileName(offset)) + if err := os.WriteFile(fileName, content, 0644); err != nil { + return fmt.Errorf("unable to create %q: %w", fileName, err) + } + } + return nil +} + +func (h *handler) Build(e assets.BuildArgs) error { + return buildLayers(e.AssetDir, path.Join(e.AssetDir, "layers.json"), e.SrcDir) +} + +func (h *handler) Info(a assets.InfoArgs) (assets.InfoResult, error) { + r := bytes.NewReader(a.StageData) + header, err := sotn.ReadStageHeader(r) + if err != nil { + return assets.InfoResult{}, err + } + l, layersRange, err := readLayers(r, header.Layers) + if err != nil { + return assets.InfoResult{}, fmt.Errorf("unable to read layers: %w", err) + } + _, tileMapsRange, err := readAllTileMaps(r, l) + if err != nil { + return assets.InfoResult{}, fmt.Errorf("unable to gather all the tile maps: %w", err) + } + tileDefs, tileDefsRange, err := readAllTiledefs(r, l) + if err != nil { + return assets.InfoResult{}, fmt.Errorf("unable to gather all the tile defs: %w", err) + } + + // check for unused tile defs (CEN has one) + for tileMapsRange.End() < tileDefsRange.Begin() { + offset := tileDefsRange.Begin().Sum(-0x10) + unusedTileDef, unusedTileDefRange, err := readTiledef(r, offset) + if err != nil { + return assets.InfoResult{}, fmt.Errorf("there is a gap between tileMaps and tileDefs: %w", err) + } + tileDefs[offset] = unusedTileDef + tileDefsRange = datarange.MergeDataRanges([]datarange.DataRange{tileDefsRange, unusedTileDefRange}) + } + + return assets.InfoResult{ + AssetEntries: []assets.InfoAssetEntry{ + { + DataRange: layersRange, + Kind: "layers", + Name: "layers", + }, + }, + SplatEntries: []assets.InfoSplatEntry{ + { + DataRange: layersRange, + Name: "header", + Comment: "layers", + }, + { + DataRange: tileMapsRange, + Name: "tile_data", + Comment: "tile data", + }, + { + DataRange: tileDefsRange, + Name: "tile_data", + Comment: "tile definitions", + }, + }, + }, nil +} + +func tilemapFileName(off psx.Addr) string { + return fmt.Sprintf("tilemap_%05X.bin", off.Real(psx.RamStageBegin)) +} + +func tiledefFileName(off psx.Addr) string { + return fmt.Sprintf("tiledef_%05X.json", off.Real(psx.RamStageBegin)) +} + +func tiledefIndicesFileName(off psx.Addr) string { + return fmt.Sprintf("tiledef_%05X_tiles.bin", off.Real(psx.RamStageBegin)) +} + +func tiledefPagesFileName(off psx.Addr) string { + return fmt.Sprintf("tiledef_%05X_pages.bin", off.Real(psx.RamStageBegin)) +} + +func tiledefClutsFileName(off psx.Addr) string { + return fmt.Sprintf("tiledef_%05X_cluts.bin", off.Real(psx.RamStageBegin)) +} + +func tiledefCollisionsFileName(off psx.Addr) string { + return fmt.Sprintf("tiledef_%05X_cols.bin", off.Real(psx.RamStageBegin)) +} diff --git a/tools/sotn-assets/build.go b/tools/sotn-assets/assets/layer/layer.go similarity index 56% rename from tools/sotn-assets/build.go rename to tools/sotn-assets/assets/layer/layer.go index 03acf52c8..1d5d38577 100644 --- a/tools/sotn-assets/build.go +++ b/tools/sotn-assets/assets/layer/layer.go @@ -1,20 +1,287 @@ -package main +package layer import ( + "encoding/binary" "encoding/json" - "errors" "fmt" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/util" "golang.org/x/sync/errgroup" - "hash/fnv" "io" - "io/fs" "os" "path" - "sort" - "strconv" + "slices" "strings" ) +type layerDef struct { + Data psx.Addr + Tiledef psx.Addr + PackedInfo uint32 + ZPriority uint16 + UnkE uint8 + UnkF uint8 +} + +type layerUnpacked struct { + Data string `json:"data"` + Tiledef string `json:"tiledef"` + Left int `json:"left"` + Top int `json:"top"` + Right int `json:"right"` + Bottom int `json:"bottom"` + ScrollMode int `json:"scrollMode"` + IsSaveRoom bool `json:"isSaveRoom"` + IsLoadingRoom bool `json:"isLoadingRoom"` + UnusedFlag bool `json:"unusedFlag"` + ZPriority int `json:"zPriority"` + UnkE int `json:"unkE"` + UnkF int `json:"unkF"` +} + +type roomLayers struct { + fg *layerDef + bg *layerDef +} + +func (l *layerDef) tilemapFileSize() int { + sx := int((l.PackedInfo >> 0) & 0x3F) + sy := int((l.PackedInfo >> 6) & 0x3F) + ex := int((l.PackedInfo >> 12) & 0x3F) + ey := int((l.PackedInfo >> 18) & 0x3F) + w := ex - sx + 1 + h := ey - sy + 1 + return w * h * 512 +} + +func (l *layerDef) unpack() layerUnpacked { + return layerUnpacked{ + Data: tilemapFileName(l.Data), + Tiledef: tiledefFileName(l.Tiledef), + Left: int((l.PackedInfo >> 0) & 0x3F), + Top: int((l.PackedInfo >> 6) & 0x3F), + Right: int((l.PackedInfo >> 12) & 0x3F), + Bottom: int((l.PackedInfo >> 18) & 0x3F), + ScrollMode: int((l.PackedInfo >> 24) & 0x1F), + IsSaveRoom: int((l.PackedInfo>>24)&0x20) != 0, + IsLoadingRoom: int((l.PackedInfo>>24)&0x40) != 0, + UnusedFlag: int((l.PackedInfo>>24)&0x80) != 0, + ZPriority: int(l.ZPriority), + UnkE: int(l.UnkE), + UnkF: int(l.UnkF), + } +} + +func (r roomLayers) MarshalJSON() ([]byte, error) { + m := map[string]interface{}{} + if r.fg != nil { + m["fg"] = r.fg.unpack() + } + if r.bg != nil { + m["bg"] = r.bg.unpack() + } + return json.Marshal(m) +} + +func readLayers(r io.ReadSeeker, off psx.Addr) ([]roomLayers, datarange.DataRange, error) { + if off == 0 { + return nil, datarange.DataRange{}, nil + } + if err := off.MoveFile(r, psx.RamStageBegin); err != nil { + return nil, datarange.DataRange{}, err + } + + // when the data starts to no longer makes sense, we can assume we reached the end of the array + var layerOffsets []psx.Addr + layersOff := make([]psx.Addr, 2) + for { + if err := binary.Read(r, binary.LittleEndian, layersOff); err != nil { + return nil, datarange.DataRange{}, err + } + if layersOff[0] <= psx.RamStageBegin || layersOff[0] >= off || + layersOff[1] <= psx.RamStageBegin || layersOff[1] >= off { + break + } + layerOffsets = append(layerOffsets, layersOff...) + } + + // Creates a map of layers, so we can re-use them when a layer is used by multiple rooms + pool := map[psx.Addr]*layerDef{} + pool[psx.Addr(0)] = nil + for _, layerOffset := range layerOffsets { + if _, exists := pool[layerOffset]; exists { + continue + } + + if err := layerOffset.MoveFile(r, psx.RamStageBegin); err != nil { + return nil, datarange.DataRange{}, err + } + var l layerDef + if err := binary.Read(r, binary.LittleEndian, &l); err != nil { + return nil, datarange.DataRange{}, err + } + if l.Data != psx.RamNull || l.Tiledef != psx.RamNull || l.PackedInfo != 0 { + pool[layerOffset] = &l + } else { + pool[layerOffset] = nil + } + } + + // creates the real array with all the layers mapped + count := len(layerOffsets) >> 1 + roomsLayers := make([]roomLayers, count) + for i := 0; i < count; i++ { + roomsLayers[i].fg = pool[layerOffsets[i*2+0]] + roomsLayers[i].bg = pool[layerOffsets[i*2+1]] + } + return roomsLayers, datarange.New(slices.Min(layerOffsets), off.Sum(count*8)), nil +} + +func buildLayers(inputDir string, fileName string, outputDir string) error { + getHash := func(l layerUnpacked) string { + return fmt.Sprintf("%s-%s-%d-%d-%d-%d", l.Data, l.Tiledef, l.Left, l.Top, l.Right, l.Bottom) + } + pack := func(l layerUnpacked) map[string]interface{} { + return map[string]interface{}{ + "data": makeSymbolFromFileName(l.Data), + "tiledef": makeSymbolFromFileName(l.Tiledef), + "params": l.Left | (l.Top << 6) | (l.Right << 12) | (l.Bottom << 18) | (l.ScrollMode << 24) | + (util.Btoi(l.IsSaveRoom) << 29) | (util.Btoi(l.IsLoadingRoom) << 30) | (util.Btoi(l.UnusedFlag) << 31), + "zPriority": l.ZPriority, + "unkE": l.UnkE, + "unkF": l.UnkF, + } + } + + data, err := os.ReadFile(fileName) + if err != nil { + return err + } + + var roomsLayers []map[string]*layerUnpacked + if err := json.Unmarshal(data, &roomsLayers); err != nil { + return err + } + + tilemaps := map[string]struct{}{} + tiledefs := map[string]struct{}{} + for _, room := range roomsLayers { + // the split on '.' is to remove the extension + if layer, found := room["fg"]; found { + tilemaps[layer.Data] = struct{}{} + tiledefs[layer.Tiledef] = struct{}{} + } + if layer, found := room["bg"]; found { + tilemaps[layer.Data] = struct{}{} + tiledefs[layer.Tiledef] = struct{}{} + } + } + + // use unused tiledefs + files, err := os.ReadDir(inputDir) + if err != nil { + return err + } + for _, file := range files { + if !file.IsDir() && strings.HasPrefix(file.Name(), "tiledef_") && strings.HasSuffix(file.Name(), ".json") { + tiledefs[file.Name()] = struct{}{} + } + } + + eg := errgroup.Group{} + for name := range tilemaps { + fullPath := path.Join(path.Dir(fileName), name) + symbol := makeSymbolFromFileName(name) + eg.Go(func() error { + return buildGenericU16(fullPath, symbol, outputDir) + }) + } + for name := range tiledefs { + fullPath := path.Join(path.Dir(fileName), name) + symbol := makeSymbolFromFileName(name) + eg.Go(func() error { + return buildTiledefs(fullPath, symbol, outputDir) + }) + } + if err := eg.Wait(); err != nil { + return err + } + + var layers []map[string]interface{} // first layer is always empty + layers = append(layers, map[string]interface{}{}) + roomLayersId := make([]int, len(roomsLayers)*2) + pool := map[string]int{} + pool[""] = 0 + for _, rl := range roomsLayers { + if l, fgFound := rl["fg"]; fgFound { + hash := getHash(*l) + if index, found := pool[hash]; !found { + pool[hash] = len(layers) + roomLayersId = append(roomLayersId, len(layers)) + layers = append(layers, pack(*l)) + } else { + roomLayersId = append(roomLayersId, index) + } + } else { + roomLayersId = append(roomLayersId, 0) + } + if l, bgFound := rl["bg"]; bgFound { + hash := getHash(*l) + if index, found := pool[hash]; !found { + pool[hash] = len(layers) + roomLayersId = append(roomLayersId, len(layers)) + layers = append(layers, pack(*l)) + } else { + roomLayersId = append(roomLayersId, index) + } + } else { + roomLayersId = append(roomLayersId, 0) + } + } + + sb := strings.Builder{} + sb.WriteString("// clang-format off\n") + for name := range tilemaps { + symbol := makeSymbolFromFileName(name) + sb.WriteString(fmt.Sprintf("extern u16 %s[];\n", symbol)) + } + for name := range tiledefs { + symbol := makeSymbolFromFileName(name) + sb.WriteString(fmt.Sprintf("extern TileDefinition %s[];\n", symbol)) + } + + sb.WriteString("static MyLayer layers[] = {\n") + sb.WriteString(" { NULL, NULL, 0, 0, 0, 0 },\n") + for _, l := range layers[1:] { + sb.WriteString(fmt.Sprintf(" { %s, %s, 0x%08X, 0x%02X, %d, %d },\n", + makeSymbolFromFileName(l["data"].(string)), + makeSymbolFromFileName(l["tiledef"].(string)), + l["params"], + l["zPriority"], + l["unkE"], + l["unkF"], + )) + } + sb.WriteString("};\n") + + sb.WriteString("MyRoomDef OVL_EXPORT(rooms_layers)[] = {\n") + for _, rl := range roomsLayers { + if l, found := rl["fg"]; found { + sb.WriteString(fmt.Sprintf(" { &layers[%d], ", pool[getHash(*l)])) + } else { + sb.WriteString(fmt.Sprintf(" { &layers[0], ")) + } + if l, found := rl["bg"]; found { + sb.WriteString(fmt.Sprintf("&layers[%d] },\n", pool[getHash(*l)])) + } else { + sb.WriteString(fmt.Sprintf("&layers[0] },\n")) + } + } + sb.WriteString("};\n") + return os.WriteFile(path.Join(outputDir, "layers.h"), []byte(sb.String()), 0644) +} + func makeSymbolFromFileName(fileName string) string { return strings.Split(path.Base(fileName), ".")[0] } @@ -111,302 +378,3 @@ func buildTiledefs(fileName string, symbol string, outputDir string) error { return nil } - -func buildLayers(inputDir string, fileName string, outputDir string) error { - getHash := func(l layerUnpacked) string { - return fmt.Sprintf("%s-%s-%d-%d-%d-%d", l.Data, l.Tiledef, l.Left, l.Top, l.Right, l.Bottom) - } - pack := func(l layerUnpacked) map[string]interface{} { - return map[string]interface{}{ - "data": makeSymbolFromFileName(l.Data), - "tiledef": makeSymbolFromFileName(l.Tiledef), - "params": l.Left | (l.Top << 6) | (l.Right << 12) | (l.Bottom << 18) | (l.ScrollMode << 24) | - (btoi(l.IsSaveRoom) << 29) | (btoi(l.IsLoadingRoom) << 30) | (btoi(l.UnusedFlag) << 31), - "zPriority": l.ZPriority, - "unkE": l.UnkE, - "unkF": l.UnkF, - } - } - - data, err := os.ReadFile(fileName) - if err != nil { - return err - } - - var roomsLayers []map[string]*layerUnpacked - if err := json.Unmarshal(data, &roomsLayers); err != nil { - return err - } - - tilemaps := map[string]struct{}{} - tiledefs := map[string]struct{}{} - for _, room := range roomsLayers { - // the split on '.' is to remove the extension - if layer, found := room["fg"]; found { - tilemaps[layer.Data] = struct{}{} - tiledefs[layer.Tiledef] = struct{}{} - } - if layer, found := room["bg"]; found { - tilemaps[layer.Data] = struct{}{} - tiledefs[layer.Tiledef] = struct{}{} - } - } - - // use unused tiledefs - files, err := os.ReadDir(inputDir) - if err != nil { - return err - } - for _, file := range files { - if !file.IsDir() && strings.HasPrefix(file.Name(), "tiledef_") && strings.HasSuffix(file.Name(), ".json") { - tiledefs[file.Name()] = struct{}{} - } - } - - eg := errgroup.Group{} - for name := range tilemaps { - fullPath := path.Join(path.Dir(fileName), name) - symbol := makeSymbolFromFileName(name) - eg.Go(func() error { - return buildGenericU16(fullPath, symbol, outputDir) - }) - } - for name := range tiledefs { - fullPath := path.Join(path.Dir(fileName), name) - symbol := makeSymbolFromFileName(name) - eg.Go(func() error { - return buildTiledefs(fullPath, symbol, outputDir) - }) - } - if err := eg.Wait(); err != nil { - return err - } - - layers := []map[string]interface{}{} // first layer is always empty - layers = append(layers, map[string]interface{}{}) - roomLayersId := make([]int, len(roomsLayers)*2) - pool := map[string]int{} - pool[""] = 0 - for _, rl := range roomsLayers { - if l, fgFound := rl["fg"]; fgFound { - hash := getHash(*l) - if index, found := pool[hash]; !found { - pool[hash] = len(layers) - roomLayersId = append(roomLayersId, len(layers)) - layers = append(layers, pack(*l)) - } else { - roomLayersId = append(roomLayersId, index) - } - } else { - roomLayersId = append(roomLayersId, 0) - } - if l, bgFound := rl["bg"]; bgFound { - hash := getHash(*l) - if index, found := pool[hash]; !found { - pool[hash] = len(layers) - roomLayersId = append(roomLayersId, len(layers)) - layers = append(layers, pack(*l)) - } else { - roomLayersId = append(roomLayersId, index) - } - } else { - roomLayersId = append(roomLayersId, 0) - } - } - - sb := strings.Builder{} - sb.WriteString("// clang-format off\n") - for name := range tilemaps { - symbol := makeSymbolFromFileName(name) - sb.WriteString(fmt.Sprintf("extern u16 %s[];\n", symbol)) - } - for name := range tiledefs { - symbol := makeSymbolFromFileName(name) - sb.WriteString(fmt.Sprintf("extern TileDefinition %s[];\n", symbol)) - } - - sb.WriteString("static MyLayer layers[] = {\n") - sb.WriteString(" { NULL, NULL, 0, 0, 0, 0 },\n") - for _, l := range layers[1:] { - sb.WriteString(fmt.Sprintf(" { %s, %s, 0x%08X, 0x%02X, %d, %d },\n", - makeSymbolFromFileName(l["data"].(string)), - makeSymbolFromFileName(l["tiledef"].(string)), - l["params"], - l["zPriority"], - l["unkE"], - l["unkF"], - )) - } - sb.WriteString("};\n") - - sb.WriteString("MyRoomDef OVL_EXPORT(rooms_layers)[] = {\n") - for _, rl := range roomsLayers { - if l, found := rl["fg"]; found { - sb.WriteString(fmt.Sprintf(" { &layers[%d], ", pool[getHash(*l)])) - } else { - sb.WriteString(fmt.Sprintf(" { &layers[0], ")) - } - if l, found := rl["bg"]; found { - sb.WriteString(fmt.Sprintf("&layers[%d] },\n", pool[getHash(*l)])) - } else { - sb.WriteString(fmt.Sprintf("&layers[0] },\n")) - } - } - sb.WriteString("};\n") - return os.WriteFile(path.Join(outputDir, "layers.h"), []byte(sb.String()), 0644) -} - -func buildEntityLayouts(fileName string, outputDir string) error { - ovlName := path.Base(outputDir) - - writeLayoutEntries := func(sb *strings.Builder, banks [][]layoutEntry, align4 bool) error { - nWritten := 0 - for i, entries := range banks { - // do a sanity check on the entries as we do not want to build something that will cause the game to crash - if entries[0].X != -2 || entries[0].Y != -2 { - return fmt.Errorf("layout entity bank %d needs to have a X:-2 and Y:-2 entry at the beginning", i) - } - lastEntry := entries[len(entries)-1] - if lastEntry.X != -1 || lastEntry.Y != -1 { - return fmt.Errorf("layout entity bank %d needs to have a X:-1 and Y:-1 entry at the end", i) - } - sb.WriteString(fmt.Sprintf("//%d\n", nWritten)) //label each block with offsets - for _, e := range entries { - var entityIDStr string - if int(e.Flags) != 0 { - // This will only ever be 0xA001. - id, _ := strconv.ParseInt(strings.Replace(e.ID, "0x", "", -1), 16, 16) - entityIDStr = fmt.Sprintf("0x%04X", (int(e.Flags)<<8)|int(id)) - } else { - entityIDStr = e.ID - } - sb.WriteString(fmt.Sprintf(" 0x%04X, 0x%04X, %s, 0x%04X, 0x%04X,\n", - uint16(e.X), uint16(e.Y), entityIDStr, int(e.Slot)|(int(e.SpawnID)<<8), e.Params)) - } - nWritten += len(entries) - } - if align4 && nWritten%2 != 0 { - sb.WriteString(" 0, // padding\n") - } - return nil - } - makeSortedBanks := func(banks [][]layoutEntry, sortByX bool) [][]layoutEntry { - var toSort []layoutEntry - var less func(i, j int) bool - if sortByX { - less = func(i, j int) bool { - return toSort[i].X < toSort[j].X - } - } else { - less = func(i, j int) bool { - if toSort[i].Y < toSort[j].Y { - return true - } - if toSort[i].Y > toSort[j].Y { - return false - } - if toSort[i].YOrder != nil && toSort[j].YOrder != nil { - return *toSort[i].YOrder < *toSort[j].YOrder - } - return i < j - } - } - sorting := make([][]layoutEntry, len(banks)) - for i, entries := range banks { - sorting[i] = make([]layoutEntry, len(entries)-2) - if len(sorting[i]) > 0 { // do not sort if the list is empty - copy(sorting[i], entries[1:len(entries)-1]) // do not sort the -2 and -1 entries - toSort = sorting[i] - sort.SliceStable(toSort, less) - } - - // put back the -2 and -1 - sorting[i] = append([]layoutEntry{entries[0]}, sorting[i]...) - sorting[i] = append(sorting[i], entries[len(entries)-1]) - } - return sorting - } - - data, err := os.ReadFile(fileName) - if err != nil { - return err - } - - var el layouts - if err := json.Unmarshal(data, &el); err != nil { - return err - } - - h := fnv.New32() - h.Write([]byte(outputDir)) - symbolVariant := strconv.FormatUint(uint64(h.Sum32()), 16) - symbolName := fmt.Sprintf("entity_layout_%s", symbolVariant) - offsets := make([]int, len(el.Entities)) - offsetCur := 0 - for i := 0; i < len(el.Entities); i++ { - offsets[i] = offsetCur - offsetCur += len(el.Entities[i]) * 5 - } - - sbHeader := strings.Builder{} - sbHeader.WriteString("#include \n\n") - sbHeader.WriteString("#include \"common.h\"\n\n") - sbHeader.WriteString("// clang-format off\n") - sbHeader.WriteString(fmt.Sprintf("extern LayoutEntity %s_x[];\n", symbolName)) - sbHeader.WriteString(fmt.Sprintf("LayoutEntity* %s_pStObjLayoutHorizontal[] = {\n", strings.ToUpper(ovlName))) - for _, i := range el.Indices { - sbHeader.WriteString(fmt.Sprintf(" &%s_x[%d],\n", symbolName, offsets[i]/5)) - } - sbHeader.WriteString(fmt.Sprintf("};\n")) - sbHeader.WriteString(fmt.Sprintf("extern LayoutEntity %s_y[];\n", symbolName)) - sbHeader.WriteString(fmt.Sprintf("LayoutEntity* %s_pStObjLayoutVertical[] = {\n", strings.ToUpper(ovlName))) - for _, i := range el.Indices { - sbHeader.WriteString(fmt.Sprintf(" &%s_y[%d],\n", symbolName, offsets[i]/5)) - } - sbHeader.WriteString(fmt.Sprintf("};\n")) - - sbData := strings.Builder{} - sbData.WriteString(fmt.Sprintf("#include \"%s.h\"\n\n", ovlName)) - sbData.WriteString("// clang-format off\n") - sbData.WriteString(fmt.Sprintf("u16 %s_x[] = {\n", symbolName)) - if err := writeLayoutEntries(&sbData, makeSortedBanks(el.Entities, true), false); err != nil { - return fmt.Errorf("unable to build X entity layout: %w", err) - } - sbData.WriteString(fmt.Sprintf("};\n")) - sbData.WriteString(fmt.Sprintf("u16 %s_y[] = {\n", symbolName)) - if err := writeLayoutEntries(&sbData, makeSortedBanks(el.Entities, false), true); err != nil { - return fmt.Errorf("unable to build Y entity layout: %w", err) - } - sbData.WriteString(fmt.Sprintf("};\n")) - - if err := os.WriteFile(path.Join(outputDir, "e_layout.c"), []byte(sbData.String()), 0644); err != nil { - return err - } - return os.WriteFile(path.Join(outputDir, "e_laydef.c"), []byte(sbHeader.String()), 0644) -} - -func buildAll(inputDir string, outputDir string) error { - if err := os.MkdirAll(outputDir, 0755); err != nil { - return err - } - - eg := errgroup.Group{} - eg.Go(func() error { - if err := buildLayers(inputDir, path.Join(inputDir, "layers.json"), outputDir); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return err - } - } - return nil - }) - eg.Go(func() error { - if err := buildEntityLayouts(path.Join(inputDir, "entity_layouts.json"), outputDir); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return err - } - } - return nil - }) - - return eg.Wait() -} diff --git a/tools/sotn-assets/tile_def.go b/tools/sotn-assets/assets/layer/tile_def.go similarity index 57% rename from tools/sotn-assets/tile_def.go rename to tools/sotn-assets/assets/layer/tile_def.go index 5b3467293..bee6d82b8 100644 --- a/tools/sotn-assets/tile_def.go +++ b/tools/sotn-assets/assets/layer/tile_def.go @@ -1,18 +1,18 @@ -package main +package layer import ( "encoding/binary" "fmt" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" - "os" + "io" ) type tileDef struct { - tiles []byte - pages []byte - cluts []byte - cols []byte + Tiles []byte + Pages []byte + Cluts []byte + Cols []byte } type tileDefPaths struct { @@ -22,61 +22,61 @@ type tileDefPaths struct { Collisions string `json:"collisions"` } -func readTiledef(file *os.File, off psx.Addr) (tileDef, datarange.DataRange, error) { - if err := off.MoveFile(file, psx.RamStageBegin); err != nil { +func readTiledef(r io.ReadSeeker, off psx.Addr) (tileDef, datarange.DataRange, error) { + if err := off.MoveFile(r, psx.RamStageBegin); err != nil { return tileDef{}, datarange.DataRange{}, err } offsets := make([]psx.Addr, 4) - if err := binary.Read(file, binary.LittleEndian, offsets); err != nil { + if err := binary.Read(r, binary.LittleEndian, offsets); err != nil { return tileDef{}, datarange.DataRange{}, err } td := tileDef{ - tiles: make([]byte, offsets[1]-offsets[0]), - pages: make([]byte, offsets[2]-offsets[1]), - cluts: make([]byte, offsets[3]-offsets[2]), - cols: make([]byte, off-offsets[3]), + Tiles: make([]byte, offsets[1]-offsets[0]), + Pages: make([]byte, offsets[2]-offsets[1]), + Cluts: make([]byte, offsets[3]-offsets[2]), + Cols: make([]byte, off-offsets[3]), } - if err := offsets[0].MoveFile(file, psx.RamStageBegin); err != nil { + if err := offsets[0].MoveFile(r, psx.RamStageBegin); err != nil { return tileDef{}, datarange.DataRange{}, err } - if _, err := file.Read(td.tiles); err != nil { + if _, err := r.Read(td.Tiles); err != nil { return tileDef{}, datarange.DataRange{}, err } - if err := offsets[1].MoveFile(file, psx.RamStageBegin); err != nil { + if err := offsets[1].MoveFile(r, psx.RamStageBegin); err != nil { return tileDef{}, datarange.DataRange{}, err } - if _, err := file.Read(td.pages); err != nil { + if _, err := r.Read(td.Pages); err != nil { return tileDef{}, datarange.DataRange{}, err } - if err := offsets[2].MoveFile(file, psx.RamStageBegin); err != nil { + if err := offsets[2].MoveFile(r, psx.RamStageBegin); err != nil { return tileDef{}, datarange.DataRange{}, err } - if _, err := file.Read(td.cluts); err != nil { + if _, err := r.Read(td.Cluts); err != nil { return tileDef{}, datarange.DataRange{}, err } - if err := offsets[3].MoveFile(file, psx.RamStageBegin); err != nil { + if err := offsets[3].MoveFile(r, psx.RamStageBegin); err != nil { return tileDef{}, datarange.DataRange{}, err } - if _, err := file.Read(td.cols); err != nil { + if _, err := r.Read(td.Cols); err != nil { return tileDef{}, datarange.DataRange{}, err } return td, datarange.New(offsets[0], off.Sum(0x10)), nil } -func readAllTiledefs(file *os.File, roomLayers []roomLayers) (map[psx.Addr]tileDef, datarange.DataRange, error) { - ranges := []datarange.DataRange{} +func readAllTiledefs(r io.ReadSeeker, roomLayers []roomLayers) (map[psx.Addr]tileDef, datarange.DataRange, error) { + var ranges []datarange.DataRange processed := map[psx.Addr]tileDef{} for _, rl := range roomLayers { if rl.fg != nil { if _, found := processed[rl.fg.Tiledef]; !found { - td, r, err := readTiledef(file, rl.fg.Tiledef) + td, r, err := readTiledef(r, rl.fg.Tiledef) if err != nil { return nil, datarange.DataRange{}, fmt.Errorf("unable to read fg tiledef: %w", err) } @@ -86,7 +86,7 @@ func readAllTiledefs(file *os.File, roomLayers []roomLayers) (map[psx.Addr]tileD } if rl.bg != nil { if _, found := processed[rl.bg.Tiledef]; !found { - td, r, err := readTiledef(file, rl.bg.Tiledef) + td, r, err := readTiledef(r, rl.bg.Tiledef) if err != nil { return nil, datarange.DataRange{}, fmt.Errorf("unable to read fg tiledef: %w", err) } diff --git a/tools/sotn-assets/tile_map.go b/tools/sotn-assets/assets/layer/tile_map.go similarity index 68% rename from tools/sotn-assets/tile_map.go rename to tools/sotn-assets/assets/layer/tile_map.go index 29c6417f2..07fe1bb0a 100644 --- a/tools/sotn-assets/tile_map.go +++ b/tools/sotn-assets/assets/layer/tile_map.go @@ -1,30 +1,30 @@ -package main +package layer import ( "fmt" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" - "os" + "io" ) -func readTilemap(file *os.File, layer *layer) ([]byte, datarange.DataRange, error) { - if err := layer.Data.MoveFile(file, psx.RamStageBegin); err != nil { +func readTilemap(r io.ReadSeeker, layer *layerDef) ([]byte, datarange.DataRange, error) { + if err := layer.Data.MoveFile(r, psx.RamStageBegin); err != nil { return nil, datarange.DataRange{}, err } data := make([]byte, layer.tilemapFileSize()) - if _, err := file.Read(data); err != nil { + if _, err := r.Read(data); err != nil { return nil, datarange.DataRange{}, err } return data, datarange.FromAddr(layer.Data, len(data)), nil } -func readAllTileMaps(file *os.File, roomLayers []roomLayers) (map[psx.Addr][]byte, datarange.DataRange, error) { - ranges := []datarange.DataRange{} +func readAllTileMaps(r io.ReadSeeker, roomLayers []roomLayers) (map[psx.Addr][]byte, datarange.DataRange, error) { + var ranges []datarange.DataRange processed := map[psx.Addr][]byte{} for _, rl := range roomLayers { if rl.fg != nil { if _, found := processed[rl.fg.Data]; !found { - td, r, err := readTilemap(file, rl.fg) + td, r, err := readTilemap(r, rl.fg) if err != nil { return nil, datarange.DataRange{}, fmt.Errorf("unable to read fg tilemap: %w", err) } @@ -34,7 +34,7 @@ func readAllTileMaps(file *os.File, roomLayers []roomLayers) (map[psx.Addr][]byt } if rl.bg != nil { if _, found := processed[rl.bg.Data]; !found { - td, r, err := readTilemap(file, rl.bg) + td, r, err := readTilemap(r, rl.bg) if err != nil { return nil, datarange.DataRange{}, fmt.Errorf("unable to read fg tilemap: %w", err) } diff --git a/tools/sotn-assets/assets/layout/handler.go b/tools/sotn-assets/assets/layout/handler.go new file mode 100644 index 000000000..cf1c40bd5 --- /dev/null +++ b/tools/sotn-assets/assets/layout/handler.go @@ -0,0 +1,101 @@ +package layout + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/graphics" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/sotn" + "io" + "os" + "path" +) + +const entryCount = 53 // the number seems to be fixed + +type handler struct{} + +var Handler = &handler{} + +func (h *handler) Name() string { return "layout" } + +func (h *handler) Extract(e assets.ExtractArgs) error { + ovlName := path.Base(e.AssetDir) + r := bytes.NewReader(e.Data) + layoutOff, err := layoutOffset(r) + if err != nil { + return err + } + layouts, _, err := readEntityLayout(r, ovlName, layoutOff, entryCount, true) + if err != nil { + return err + } + content, err := json.MarshalIndent(layouts, "", " ") + if err != nil { + return err + } + return os.WriteFile(assetPath(e.AssetDir, e.Name), content, 0644) +} + +func (h *handler) Build(e assets.BuildArgs) error { + return buildEntityLayouts(assetPath(e.AssetDir, e.Name), e.SrcDir) +} + +func assetPath(dir, name string) string { + return path.Join(dir, fmt.Sprintf("%s.json", name)) +} + +func (h *handler) Info(a assets.InfoArgs) (assets.InfoResult, error) { + r := bytes.NewReader(a.StageData) + layoutOff, err := layoutOffset(r) + if err != nil { + return assets.InfoResult{}, err + } + nLayouts := 53 // it seems there are always 53 elements?! + _, layoutsRange, err := readEntityLayout(r, "dummy", layoutOff, nLayouts, true) + if err != nil { + return assets.InfoResult{}, fmt.Errorf("unable to gather all entity layouts: %w", err) + } + return assets.InfoResult{ + AssetEntries: []assets.InfoAssetEntry{ + { + DataRange: layoutsRange[0], + Kind: "layout", + Name: "entity_layouts", + }, + }, + SplatEntries: []assets.InfoSplatEntry{ + { + DataRange: layoutsRange[0], + Name: "e_laydef", + Comment: "layout entries header", + }, + { + DataRange: layoutsRange[1], + Name: "e_layout", + Comment: "layout entries data", + }, + }, + }, nil +} + +func layoutOffset(r io.ReadSeeker) (psx.Addr, error) { + header, err := sotn.ReadStageHeader(r) + if err != nil { + return psx.RamNull, err + } + if header.Layouts != psx.RamNull { + return header.Layouts, nil + } + + // ⚠️ assumption + // some overlays have this field nulled, we have to find the offset ourselves + // it should be usually be right after header.Graphics + _, graphicsRange, err := graphics.ReadGraphics(r, header.Graphics) + if err != nil { + return psx.RamNull, fmt.Errorf("unable to gather all graphics: %w", err) + } + return graphicsRange.End(), nil +} diff --git a/tools/sotn-assets/assets/layout/layout.go b/tools/sotn-assets/assets/layout/layout.go new file mode 100644 index 000000000..c26de9892 --- /dev/null +++ b/tools/sotn-assets/assets/layout/layout.go @@ -0,0 +1,305 @@ +package layout + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/sotn" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/util" + "hash/fnv" + "io" + "os" + "path" + "sort" + "strconv" + "strings" +) + +type layoutEntry struct { + X int16 `json:"x"` + Y int16 `json:"y"` + ID string `json:"id"` + Flags uint8 `json:"flags"` // TODO properly de-serialize this + Slot uint8 `json:"slot"` + SpawnID uint8 `json:"spawnId"` + Params uint16 `json:"params"` + YOrder *int `json:"yOrder,omitempty"` +} + +type layouts struct { + Entities [][]layoutEntry `json:"entities"` + Indices []int `json:"indices"` +} + +func fetchEntityIDsFromHeaderFile(overlay string) (map[int]string, error) { + f, err := os.Open("src/st/" + overlay + "/" + overlay + ".h") + if err != nil { + return nil, err + } + defer f.Close() + return sotn.ParseCEnum(f, "EntityIDs") +} + +func readEntityLayoutEntry(file io.ReadSeeker, ovlName string) (layoutEntry, error) { + entityIDs, _ := fetchEntityIDsFromHeaderFile(ovlName) + + bs := make([]byte, 10) + if _, err := io.ReadFull(file, bs); err != nil { + return layoutEntry{}, err + } + + var entityIDStr string + id := int(bs[4]) + // Try to load the proper enum + entityIDStr = entityIDs[id] + // If enum unknown or flags are set, override, don't use enums + if entityIDStr == "" || bs[5] != 0 { + entityIDStr = fmt.Sprintf("0x%02X", id) + } + + return layoutEntry{ + X: int16(binary.LittleEndian.Uint16(bs[0:2])), + Y: int16(binary.LittleEndian.Uint16(bs[2:4])), + ID: entityIDStr, + Flags: bs[5], + Slot: bs[6], + SpawnID: bs[7], + Params: binary.LittleEndian.Uint16(bs[8:10]), + }, nil +} + +// the Y-ordered entries list has a different order than the X-ordered one. The order cannot consistently get +// restored by just sorting entries by Y as usually entries with the same Y results swapped. +// This algorithm will fill the optional field YOrder, only useful to restore the original order. +func hydrateYOrderFields(x layouts, y layouts) error { + if len(x.Indices) != len(y.Indices) { + return fmt.Errorf("number of X and Y layout indices do not match") + } + if len(x.Entities) != len(y.Entities) { + return fmt.Errorf("number of X and Y layout entries do not match") + } + + populateYOrderField := func(xEntries []layoutEntry, yEntries []layoutEntry) { + yIndexMap := make(map[layoutEntry]int, len(yEntries)) + for i, e := range yEntries { + yIndexMap[e] = i + } + for i := 0; i < len(xEntries); i++ { + if yOrder, found := yIndexMap[xEntries[i]]; found { + xEntries[i].YOrder = &yOrder + } + } + } + + for i := 0; i < len(x.Entities); i++ { + xList := x.Entities[i] + yList := y.Entities[i] + if len(xList) != len(yList) { + return fmt.Errorf("number of X and Y entries do not match") + } + populateYOrderField(xList, yList) + } + return nil +} + +func readEntityLayout(r io.ReadSeeker, ovlName string, off psx.Addr, count int, isX bool) (layouts, []datarange.DataRange, error) { + if err := off.MoveFile(r, psx.RamStageBegin); err != nil { + return layouts{}, nil, err + } + + // there are two copies of the layout, one ordered by X and the other one ordered by Y + // we will only read the first one, which is ordered by Y + blockOffsets := make([]psx.Addr, count) + if err := binary.Read(r, binary.LittleEndian, blockOffsets); err != nil { + return layouts{}, nil, err + } + + // the order of each layout entry must be preserved + pool := map[psx.Addr]int{} + var blocks [][]layoutEntry + var xRanges []datarange.DataRange + for _, blockOffset := range util.SortUniqueOffsets(blockOffsets) { + if err := blockOffset.MoveFile(r, psx.RamStageBegin); err != nil { + return layouts{}, nil, err + } + var entries []layoutEntry + for { + entry, err := readEntityLayoutEntry(r, ovlName) + if err != nil { + return layouts{}, nil, err + } + if entry.X == -1 && entry.Y == -1 { + entries = append(entries, entry) + break + } + entries = append(entries, entry) + } + + // sanity check on the first entry + if entries[0].X != -2 || entries[0].Y != -2 { + err := fmt.Errorf("first layout entry does not mark the beginning of the array: %v", entries[0]) + return layouts{}, nil, err + } + + pool[blockOffset] = len(blocks) + blocks = append(blocks, entries) + xRanges = append(xRanges, datarange.FromAddr(blockOffset, len(entries)*10)) + } + // the very last entry needs to be aligned by 4 + xRanges[len(xRanges)-1] = xRanges[len(xRanges)-1].Align4() + + l := layouts{Entities: blocks} + for _, blockOffset := range blockOffsets { + l.Indices = append(l.Indices, pool[blockOffset]) + } + + endOfArray := off.Sum(count * 4) + if isX { // we want to do the same thing with the vertically aligned layout + yLayouts, yRanges, err := readEntityLayout(r, ovlName, endOfArray, count, false) + if err != nil { + return layouts{}, nil, fmt.Errorf("readEntityLayout failed on Y: %w", err) + } + if err := hydrateYOrderFields(l, yLayouts); err != nil { + return layouts{}, nil, fmt.Errorf("unable to populate YOrder field: %w", err) + } + xMerged := datarange.MergeDataRanges(xRanges) + yMerged := yRanges[1] + return l, []datarange.DataRange{ + datarange.MergeDataRanges([]datarange.DataRange{datarange.New(off, endOfArray), yRanges[0]}), + datarange.MergeDataRanges([]datarange.DataRange{xMerged, yMerged}), + }, nil + } else { + return l, []datarange.DataRange{datarange.New(off, endOfArray), datarange.MergeDataRanges(xRanges)}, nil + } +} + +func buildEntityLayouts(fileName string, outputDir string) error { + ovlName := path.Base(outputDir) + + writeLayoutEntries := func(sb *strings.Builder, banks [][]layoutEntry, align4 bool) error { + nWritten := 0 + for i, entries := range banks { + // do a sanity check on the entries as we do not want to build something that will cause the game to crash + if entries[0].X != -2 || entries[0].Y != -2 { + return fmt.Errorf("layout entity bank %d needs to have a X:-2 and Y:-2 entry at the beginning", i) + } + lastEntry := entries[len(entries)-1] + if lastEntry.X != -1 || lastEntry.Y != -1 { + return fmt.Errorf("layout entity bank %d needs to have a X:-1 and Y:-1 entry at the end", i) + } + sb.WriteString(fmt.Sprintf("//%d\n", nWritten)) //label each block with offsets + for _, e := range entries { + var entityIDStr string + if int(e.Flags) != 0 { + // This will only ever be 0xA001. + id, _ := strconv.ParseInt(strings.Replace(e.ID, "0x", "", -1), 16, 16) + entityIDStr = fmt.Sprintf("0x%04X", (int(e.Flags)<<8)|int(id)) + } else { + entityIDStr = e.ID + } + sb.WriteString(fmt.Sprintf(" 0x%04X, 0x%04X, %s, 0x%04X, 0x%04X,\n", + uint16(e.X), uint16(e.Y), entityIDStr, int(e.Slot)|(int(e.SpawnID)<<8), e.Params)) + } + nWritten += len(entries) + } + if align4 && nWritten%2 != 0 { + sb.WriteString(" 0, // padding\n") + } + return nil + } + makeSortedBanks := func(banks [][]layoutEntry, sortByX bool) [][]layoutEntry { + var toSort []layoutEntry + var less func(i, j int) bool + if sortByX { + less = func(i, j int) bool { + return toSort[i].X < toSort[j].X + } + } else { + less = func(i, j int) bool { + if toSort[i].Y < toSort[j].Y { + return true + } + if toSort[i].Y > toSort[j].Y { + return false + } + if toSort[i].YOrder != nil && toSort[j].YOrder != nil { + return *toSort[i].YOrder < *toSort[j].YOrder + } + return i < j + } + } + sorting := make([][]layoutEntry, len(banks)) + for i, entries := range banks { + sorting[i] = make([]layoutEntry, len(entries)-2) + if len(sorting[i]) > 0 { // do not sort if the list is empty + copy(sorting[i], entries[1:len(entries)-1]) // do not sort the -2 and -1 entries + toSort = sorting[i] + sort.SliceStable(toSort, less) + } + + // put back the -2 and -1 + sorting[i] = append([]layoutEntry{entries[0]}, sorting[i]...) + sorting[i] = append(sorting[i], entries[len(entries)-1]) + } + return sorting + } + + data, err := os.ReadFile(fileName) + if err != nil { + return err + } + + var el layouts + if err := json.Unmarshal(data, &el); err != nil { + return err + } + + h := fnv.New32() + _, _ = h.Write([]byte(outputDir)) + symbolVariant := strconv.FormatUint(uint64(h.Sum32()), 16) + symbolName := fmt.Sprintf("entity_layout_%s", symbolVariant) + offsets := make([]int, len(el.Entities)) + offsetCur := 0 + for i := 0; i < len(el.Entities); i++ { + offsets[i] = offsetCur + offsetCur += len(el.Entities[i]) * 5 + } + + sbHeader := strings.Builder{} + sbHeader.WriteString("#include \n\n") + sbHeader.WriteString("#include \"common.h\"\n\n") + sbHeader.WriteString("// clang-format off\n") + sbHeader.WriteString(fmt.Sprintf("extern LayoutEntity %s_x[];\n", symbolName)) + sbHeader.WriteString(fmt.Sprintf("LayoutEntity* %s_pStObjLayoutHorizontal[] = {\n", strings.ToUpper(ovlName))) + for _, i := range el.Indices { + sbHeader.WriteString(fmt.Sprintf(" &%s_x[%d],\n", symbolName, offsets[i]/5)) + } + sbHeader.WriteString(fmt.Sprintf("};\n")) + sbHeader.WriteString(fmt.Sprintf("extern LayoutEntity %s_y[];\n", symbolName)) + sbHeader.WriteString(fmt.Sprintf("LayoutEntity* %s_pStObjLayoutVertical[] = {\n", strings.ToUpper(ovlName))) + for _, i := range el.Indices { + sbHeader.WriteString(fmt.Sprintf(" &%s_y[%d],\n", symbolName, offsets[i]/5)) + } + sbHeader.WriteString(fmt.Sprintf("};\n")) + + sbData := strings.Builder{} + sbData.WriteString(fmt.Sprintf("#include \"%s.h\"\n\n", ovlName)) + sbData.WriteString("// clang-format off\n") + sbData.WriteString(fmt.Sprintf("u16 %s_x[] = {\n", symbolName)) + if err := writeLayoutEntries(&sbData, makeSortedBanks(el.Entities, true), false); err != nil { + return fmt.Errorf("unable to build X entity layout: %w", err) + } + sbData.WriteString(fmt.Sprintf("};\n")) + sbData.WriteString(fmt.Sprintf("u16 %s_y[] = {\n", symbolName)) + if err := writeLayoutEntries(&sbData, makeSortedBanks(el.Entities, false), true); err != nil { + return fmt.Errorf("unable to build Y entity layout: %w", err) + } + sbData.WriteString(fmt.Sprintf("};\n")) + + if err := os.WriteFile(path.Join(outputDir, "e_layout.c"), []byte(sbData.String()), 0644); err != nil { + return err + } + return os.WriteFile(path.Join(outputDir, "e_laydef.c"), []byte(sbHeader.String()), 0644) +} diff --git a/tools/sotn-assets/assets/rooms/handler.go b/tools/sotn-assets/assets/rooms/handler.go index 30a8584ab..2117d6f0f 100644 --- a/tools/sotn-assets/assets/rooms/handler.go +++ b/tools/sotn-assets/assets/rooms/handler.go @@ -8,6 +8,7 @@ import ( "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/sotn" "io" "os" "path" @@ -32,9 +33,9 @@ var Handler = &handler{} func (h *handler) Name() string { return "rooms" } -func (h *handler) Extract(e assets.ExtractEntry) error { +func (h *handler) Extract(e assets.ExtractArgs) error { r := bytes.NewReader(e.Data) - rooms, _, err := ReadRooms(r, e.RamBase.Sum(e.Start)) + rooms, _, err := readRooms(r, e.RamBase.Sum(e.Start)) if err != nil { return fmt.Errorf("failed to read rooms: %w", err) } @@ -51,7 +52,7 @@ func (h *handler) Extract(e assets.ExtractEntry) error { return os.WriteFile(outFileName, content, 0644) } -func (h *handler) Build(e assets.BuildEntry) error { +func (h *handler) Build(e assets.BuildArgs) error { inPath := assetPath(e.AssetDir, e.Name) outPath := sourcePath(e.SrcDir, e.Name) ovlName := path.Base(path.Dir(outPath)) @@ -78,6 +79,33 @@ func (h *handler) Build(e assets.BuildEntry) error { return os.WriteFile(outPath, []byte(content.String()), 0644) } +func (h *handler) Info(a assets.InfoArgs) (assets.InfoResult, error) { + r := bytes.NewReader(a.StageData) + header, err := sotn.ReadStageHeader(r) + if err != nil { + return assets.InfoResult{}, err + } + _, dataRange, err := readRooms(r, header.Rooms) + if err != nil { + return assets.InfoResult{}, err + } + return assets.InfoResult{ + AssetEntries: []assets.InfoAssetEntry{ + { + DataRange: dataRange, + Kind: "rooms", + Name: "rooms", + }, + }, + SplatEntries: []assets.InfoSplatEntry{ + { + DataRange: dataRange, + Name: "rooms", + }, + }, + }, nil +} + func assetPath(dir, name string) string { return path.Join(dir, fmt.Sprintf("%s.json", name)) } @@ -90,7 +118,7 @@ func (r Room) isTerminator() bool { return r.Left == 0x40 } -func ReadRooms(r io.ReadSeeker, off psx.Addr) ([]Room, datarange.DataRange, error) { +func readRooms(r io.ReadSeeker, off psx.Addr) ([]Room, datarange.DataRange, error) { if off == 0 { return nil, datarange.DataRange{}, nil } diff --git a/tools/sotn-assets/assets/skip/handler.go b/tools/sotn-assets/assets/skip/handler.go new file mode 100644 index 000000000..bc83b59c6 --- /dev/null +++ b/tools/sotn-assets/assets/skip/handler.go @@ -0,0 +1,21 @@ +package skip + +import "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets" + +type handler struct{} + +var Handler = &handler{} + +func (h *handler) Name() string { return "skip" } + +func (h *handler) Extract(e assets.ExtractArgs) error { + return nil +} + +func (h *handler) Build(e assets.BuildArgs) error { + return nil +} + +func (h *handler) Info(a assets.InfoArgs) (assets.InfoResult, error) { + return assets.InfoResult{}, nil +} diff --git a/tools/sotn-assets/assets/spritebanks/handler.go b/tools/sotn-assets/assets/spritebanks/handler.go index 425293d6f..1daa4e166 100644 --- a/tools/sotn-assets/assets/spritebanks/handler.go +++ b/tools/sotn-assets/assets/spritebanks/handler.go @@ -5,18 +5,23 @@ import ( "encoding/json" "fmt" "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/sotn" "os" "path" "path/filepath" ) +const banksCount = 24 // the number seems to be fixed + type handler struct{} var Handler = &handler{} -func (h *handler) Name() string { return "sprites" } +func (h *handler) Name() string { return "sprite_banks" } -func (h *handler) Extract(e assets.ExtractEntry) error { +func (h *handler) Extract(e assets.ExtractArgs) error { if e.Start == e.End { return fmt.Errorf("a group of sprites cannot be 0 bytes long") } @@ -38,10 +43,37 @@ func (h *handler) Extract(e assets.ExtractEntry) error { return os.WriteFile(outFileName, content, 0644) } -func (h *handler) Build(e assets.BuildEntry) error { +func (h *handler) Build(e assets.BuildArgs) error { return buildSprites(assetPath(e.AssetDir, e.Name), e.SrcDir) } +func (h *handler) Info(a assets.InfoArgs) (assets.InfoResult, error) { + r := bytes.NewReader(a.StageData) + header, err := sotn.ReadStageHeader(r) + if err != nil { + return assets.InfoResult{}, err + } + _, dataRange, err := ReadSpritesBanks(r, psx.RamStageBegin, header.Sprites) + if err != nil { + return assets.InfoResult{}, err + } + return assets.InfoResult{ + AssetEntries: []assets.InfoAssetEntry{ + { + DataRange: datarange.FromAddr(header.Sprites, banksCount*4), + Kind: "sprite_banks", + Name: "sprite_banks", + }, + }, + SplatEntries: []assets.InfoSplatEntry{ + { + DataRange: dataRange, + Name: "sprites", + }, + }, + }, nil +} + func assetPath(dir, name string) string { if name == "" { name = "sprite_banks" diff --git a/tools/sotn-assets/assets/spritebanks/spritebanks.go b/tools/sotn-assets/assets/spritebanks/spritebanks.go index 338a4c740..9ef5dbb65 100644 --- a/tools/sotn-assets/assets/spritebanks/spritebanks.go +++ b/tools/sotn-assets/assets/spritebanks/spritebanks.go @@ -23,8 +23,7 @@ func ReadSpritesBanks(r io.ReadSeeker, baseAddr, addr psx.Addr) (SpriteBanks, da return SpriteBanks{}, datarange.DataRange{}, err } - // start with a capacity of 24 as that'sprites the length for all the stage overlays - offBanks := make([]psx.Addr, 0, 24) + offBanks := make([]psx.Addr, 0, banksCount) for { addr := psx.ReadAddr(r) if addr != psx.RamNull && !addr.InRange(baseAddr, psx.RamGameEnd) { @@ -95,6 +94,9 @@ func buildSprites(fileName string, outputDir string) error { if err := json.Unmarshal(data, &spritesBanks); err != nil { return err } + if len(spritesBanks.Indices) != banksCount { + return fmt.Errorf("the number of banks must be exactly %d, got %d", banksCount, len(spritesBanks.Banks)) + } sbHeader := strings.Builder{} sbHeader.WriteString("// clang-format off\n") diff --git a/tools/sotn-assets/assets/spriteset/handler.go b/tools/sotn-assets/assets/spriteset/handler.go index 3c73357f6..6504132a0 100644 --- a/tools/sotn-assets/assets/spriteset/handler.go +++ b/tools/sotn-assets/assets/spriteset/handler.go @@ -15,9 +15,9 @@ type handler struct{} var Handler = &handler{} -func (h *handler) Name() string { return "sprites" } +func (h *handler) Name() string { return "spriteset" } -func (h *handler) Extract(e assets.ExtractEntry) error { +func (h *handler) Extract(e assets.ExtractArgs) error { if e.Name == "" { return fmt.Errorf("data at 0x%X must have a name", e.Start) } @@ -46,7 +46,7 @@ func (h *handler) Extract(e assets.ExtractEntry) error { return os.WriteFile(outFileName, content, 0644) } -func (h *handler) Build(e assets.BuildEntry) error { +func (h *handler) Build(e assets.BuildArgs) error { in := assetPath(e.AssetDir, e.Name) out := sourcePath(e.SrcDir, e.Name) data, err := os.ReadFile(in) @@ -66,6 +66,10 @@ func (h *handler) Build(e assets.BuildEntry) error { return os.WriteFile(out, []byte(sb.String()), 0644) } +func (h *handler) Info(a assets.InfoArgs) (assets.InfoResult, error) { + return assets.InfoResult{}, nil +} + func assetPath(dir, name string) string { return path.Join(dir, fmt.Sprintf("%s.animset.json", name)) } diff --git a/tools/sotn-assets/go.mod b/tools/sotn-assets/go.mod index a2e45ca87..ae3b289fc 100644 --- a/tools/sotn-assets/go.mod +++ b/tools/sotn-assets/go.mod @@ -3,6 +3,13 @@ module github.com/xeeynamo/sotn-decomp/tools/sotn-assets go 1.22 require ( - golang.org/x/sync v0.7.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/stretchr/testify v1.9.0 + golang.org/x/sync v0.7.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/sotn-assets/go.sum b/tools/sotn-assets/go.sum index 1515bf1c9..5bc23fc17 100644 --- a/tools/sotn-assets/go.sum +++ b/tools/sotn-assets/go.sum @@ -1,5 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/sotn-assets/graphics.go b/tools/sotn-assets/graphics.go deleted file mode 100644 index 3ec06c40e..000000000 --- a/tools/sotn-assets/graphics.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "encoding/binary" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" - "os" -) - -type gfxKind uint16 - -const ( - gfxBankNone = gfxKind(iota) - gfxBank4bpp - gfxBank8bpp - gfxBank16bpp - gfxBankCompressed -) - -type gfxEntry struct { - X uint16 - Y uint16 - Width uint16 - Height uint16 - Data psx.Addr -} - -type gfxBlock struct { - kind gfxKind - flags uint16 - entries []gfxEntry -} - -type gfx struct { - blocks []gfxBlock - indices []int -} - -func readGraphics(file *os.File, off psx.Addr) (gfx, datarange.DataRange, error) { - if err := off.MoveFile(file, psx.RamStageBegin); err != nil { - return gfx{}, datarange.DataRange{}, err - } - - // all the offsets are before the array, so it is easy to find where the offsets array ends - blockOffsets := []psx.Addr{} - for { - var offBank psx.Addr - if err := binary.Read(file, binary.LittleEndian, &offBank); err != nil { - return gfx{}, datarange.DataRange{}, err - } - if offBank >= off { - break - } - blockOffsets = append(blockOffsets, offBank) - } - - // the order of each gfxBlock must be preserved - pool := map[psx.Addr]int{} - pool[psx.RamNull] = -1 - blocks := []gfxBlock{} - ranges := []datarange.DataRange{} - for _, blockOffset := range sortUniqueOffsets(blockOffsets) { - if blockOffset == psx.RamNull { // exception for ST0 - continue - } - if err := blockOffset.MoveFile(file, psx.RamStageBegin); err != nil { - return gfx{}, datarange.DataRange{}, err - } - var block gfxBlock - if err := binary.Read(file, binary.LittleEndian, &block.kind); err != nil { - return gfx{}, datarange.DataRange{}, err - } - if err := binary.Read(file, binary.LittleEndian, &block.flags); err != nil { - return gfx{}, datarange.DataRange{}, err - } - - if block.kind == gfxKind(0xFFFF) && block.flags == 0xFFFF { // exception for ST0 - pool[blockOffset] = len(blocks) - blocks = append(blocks, block) - ranges = append(ranges, datarange.FromAddr(blockOffset, 4)) - continue - } - - for { - var entry gfxEntry - if err := binary.Read(file, binary.LittleEndian, &entry); err != nil { - return gfx{}, datarange.DataRange{}, err - } - if entry.X == 0xFFFF && entry.Y == 0xFFFF { - break - } - block.entries = append(block.entries, entry) - } - pool[blockOffset] = len(blocks) - blocks = append(blocks, block) - ranges = append(ranges, datarange.FromAddr(blockOffset, 4+len(block.entries)*12+4)) - } - - var g gfx - for _, blockOffset := range blockOffsets { - g.indices = append(g.indices, pool[blockOffset]) - } - - return g, datarange.MergeDataRanges(append(ranges, datarange.FromAddr(off, len(blockOffsets)*4))), nil -} diff --git a/tools/sotn-assets/info.go b/tools/sotn-assets/info.go new file mode 100644 index 000000000..9003b48dd --- /dev/null +++ b/tools/sotn-assets/info.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "io" + "os" + "sort" +) + +func info(w io.Writer, filePath string) error { + data, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("unable to read file %q: %s", filePath, err) + } + + var assetEntries []assets.InfoAssetEntry + var splatEntries []assets.InfoSplatEntry + for _, h := range handlers { + info, err := h.Info(assets.InfoArgs{ + StageFilePath: filePath, + StageData: data, + }) + if err != nil { + return fmt.Errorf("unable to gather info for file %q: %s", filePath, err) + } + assetEntries = append(assetEntries, info.AssetEntries...) + splatEntries = append(splatEntries, info.SplatEntries...) + } + _, _ = fmt.Fprintln(w, "asset config hints:") + infoAssetEntries(w, assetEntries) + _, _ = fmt.Fprintln(w, "splat config hints:") + infoSplatEntries(w, splatEntries) + return nil +} + +func infoAssetEntries(w io.Writer, entries []assets.InfoAssetEntry) { + if len(entries) == 0 { + return + } + sort.Slice(entries, func(i, j int) bool { + return entries[i].DataRange.Begin() < entries[j].DataRange.Begin() + }) + _, _ = fmt.Fprintln(w, " - [0x0, .data, header]") + for i, e := range entries { + _, _ = fmt.Fprintf(w, " - [0x%X, %s, %s]\n", e.DataRange.Begin().Real(psx.RamStageBegin), e.Kind, e.Name) + // if there is a gap between the current entry and the next one, mark it as unrecognized data + if i == len(entries)-1 || e.DataRange.End() != entries[i+1].DataRange.Begin() { + _, _ = fmt.Fprintf(w, " - [0x%X, skip]\n", e.DataRange.End().Real(psx.RamStageBegin)) + } + } +} + +func infoSplatEntries(w io.Writer, entries []assets.InfoSplatEntry) { + if len(entries) == 0 { + return + } + sort.Slice(entries, func(i, j int) bool { + return entries[i].DataRange.Begin() < entries[j].DataRange.Begin() + }) + for i, e := range entries { + s := fmt.Sprintf(" - [0x%X, .data, %s]", e.DataRange.Begin().Real(psx.RamStageBegin), e.Name) + if e.Comment != "" { + s = fmt.Sprintf("%s # %s", s, e.Comment) + } + _, _ = fmt.Fprintln(w, s) + + // if there is a gap between the current entry and the next one, mark it as unrecognized data + if i == len(entries)-1 || e.DataRange.End() != entries[i+1].DataRange.Begin() { + _, _ = fmt.Fprintf(w, " - [0x%X, data]\n", e.DataRange.End().Real(psx.RamStageBegin)) + } + } +} diff --git a/tools/sotn-assets/info_test.go b/tools/sotn-assets/info_test.go new file mode 100644 index 000000000..a161a9585 --- /dev/null +++ b/tools/sotn-assets/info_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "bytes" + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "path" + "sync" + "testing" +) + +var chdirMutex sync.Mutex + +func TestGatherAssetInfo(t *testing.T) { + changeDirToRepoRoot() + t.Run("for NZ0", func(t *testing.T) { + buf := new(bytes.Buffer) + require.NoError(t, info(buf, "disks/us/ST/NZ0/NZ0.BIN")) + stdout := buf.String() + t.Run("asset config hints", func(t *testing.T) { + assert.Contains(t, stdout, "asset config hints:\n") + assert.Contains(t, stdout, " - [0x2C, sprite_banks, sprite_banks]") + assert.Contains(t, stdout, " - [0x8C, skip]") + assert.Contains(t, stdout, " - [0x164, layers, layers]\n") + assert.Contains(t, stdout, " - [0x8EC, layout, entity_layouts]\n") + assert.Contains(t, stdout, " - [0x272C, rooms, rooms]") + assert.Contains(t, stdout, " - [0x2830, skip]") + if t.Failed() { + require.FailNow(t, "unexpected output", stdout) + } + }) + t.Run("splat config hints", func(t *testing.T) { + assert.Contains(t, stdout, "splat config hints:\n") + assert.Contains(t, stdout, " - [0x0, .data, header]\n") + assert.Contains(t, stdout, " - [0x164, .data, header] # layers\n") + assert.Contains(t, stdout, " - [0x8EC, .data, e_laydef] # layout entries header\n") + assert.Contains(t, stdout, " - [0xA94, data]\n") + assert.Contains(t, stdout, " - [0x272C, .data, rooms]\n") + assert.Contains(t, stdout, " - [0x2830, data]\n") + assert.Contains(t, stdout, " - [0x2884, .data, e_layout] # layout entries data\n") + assert.Contains(t, stdout, " - [0x3B0C, data]\n") + assert.Contains(t, stdout, " - [0x16A5C, .data, tile_data] # tile data\n") + assert.Contains(t, stdout, " - [0x20A5C, .data, tile_data] # tile definitions\n") + assert.Contains(t, stdout, " - [0x26E8C, .data, sprites]\n") + assert.Contains(t, stdout, " - [0x3058C, data]\n") + if t.Failed() { + require.FailNow(t, "unexpected output", stdout) + } + }) + }) +} + +func changeDirToRepoRoot() { + chdirMutex.Lock() + defer chdirMutex.Unlock() + for { + stat, err := os.Stat("disks/us/DRA.BIN") + if err == nil && !stat.IsDir() { + return + } + if !os.IsNotExist(err) { + panic(err) + } + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + parent := path.Dir(cwd) + if cwd == parent { + panic(fmt.Errorf("unable to find repo root")) + } + if err := os.Chdir(".."); err != nil { + panic(err) + } + } +} diff --git a/tools/sotn-assets/layer.go b/tools/sotn-assets/layer.go deleted file mode 100644 index 76e4fb7f9..000000000 --- a/tools/sotn-assets/layer.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "encoding/binary" - "encoding/json" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" - "os" - "slices" -) - -type layer struct { - Data psx.Addr - Tiledef psx.Addr - PackedInfo uint32 - ZPriority uint16 - UnkE uint8 - UnkF uint8 -} - -type layerUnpacked struct { - Data string `json:"data"` - Tiledef string `json:"tiledef"` - Left int `json:"left"` - Top int `json:"top"` - Right int `json:"right"` - Bottom int `json:"bottom"` - ScrollMode int `json:"scrollMode"` - IsSaveRoom bool `json:"isSaveRoom"` - IsLoadingRoom bool `json:"isLoadingRoom"` - UnusedFlag bool `json:"unusedFlag"` - ZPriority int `json:"zPriority"` - UnkE int `json:"unkE"` - UnkF int `json:"unkF"` -} - -type roomLayers struct { - fg *layer - bg *layer -} - -func (l *layer) tilemapFileSize() int { - sx := int((l.PackedInfo >> 0) & 0x3F) - sy := int((l.PackedInfo >> 6) & 0x3F) - ex := int((l.PackedInfo >> 12) & 0x3F) - ey := int((l.PackedInfo >> 18) & 0x3F) - w := ex - sx + 1 - h := ey - sy + 1 - return w * h * 512 -} - -func (l *layer) unpack() layerUnpacked { - return layerUnpacked{ - Data: getTilemapFileName(l.Data), - Tiledef: getTiledefFileName(l.Tiledef), - Left: int((l.PackedInfo >> 0) & 0x3F), - Top: int((l.PackedInfo >> 6) & 0x3F), - Right: int((l.PackedInfo >> 12) & 0x3F), - Bottom: int((l.PackedInfo >> 18) & 0x3F), - ScrollMode: int((l.PackedInfo >> 24) & 0x1F), - IsSaveRoom: int((l.PackedInfo>>24)&0x20) != 0, - IsLoadingRoom: int((l.PackedInfo>>24)&0x40) != 0, - UnusedFlag: int((l.PackedInfo>>24)&0x80) != 0, - ZPriority: int(l.ZPriority), - UnkE: int(l.UnkE), - UnkF: int(l.UnkF), - } -} - -func (r roomLayers) MarshalJSON() ([]byte, error) { - m := map[string]interface{}{} - if r.fg != nil { - m["fg"] = r.fg.unpack() - } - if r.bg != nil { - m["bg"] = r.bg.unpack() - } - return json.Marshal(m) -} - -func readLayers(file *os.File, off psx.Addr) ([]roomLayers, datarange.DataRange, error) { - if off == 0 { - return nil, datarange.DataRange{}, nil - } - if err := off.MoveFile(file, psx.RamStageBegin); err != nil { - return nil, datarange.DataRange{}, err - } - - // when the data starts to no longer makes sense, we can assume we reached the end of the array - layerOffsets := []psx.Addr{} - layersOff := make([]psx.Addr, 2) - for { - if err := binary.Read(file, binary.LittleEndian, layersOff); err != nil { - return nil, datarange.DataRange{}, err - } - if layersOff[0] <= psx.RamStageBegin || layersOff[0] >= off || - layersOff[1] <= psx.RamStageBegin || layersOff[1] >= off { - break - } - layerOffsets = append(layerOffsets, layersOff...) - } - - // Creates a map of layers, so we can re-use them when a layer is used by multiple rooms - pool := map[psx.Addr]*layer{} - pool[psx.Addr(0)] = nil - for _, layerOffset := range layerOffsets { - if _, exists := pool[layerOffset]; exists { - continue - } - - if err := layerOffset.MoveFile(file, psx.RamStageBegin); err != nil { - return nil, datarange.DataRange{}, err - } - var l layer - if err := binary.Read(file, binary.LittleEndian, &l); err != nil { - return nil, datarange.DataRange{}, err - } - if l.Data != psx.RamNull || l.Tiledef != psx.RamNull || l.PackedInfo != 0 { - pool[layerOffset] = &l - } else { - pool[layerOffset] = nil - } - } - - // creates the real array with all the layers mapped - count := len(layerOffsets) >> 1 - roomsLayers := make([]roomLayers, count) - for i := 0; i < count; i++ { - roomsLayers[i].fg = pool[layerOffsets[i*2+0]] - roomsLayers[i].bg = pool[layerOffsets[i*2+1]] - } - return roomsLayers, datarange.New(slices.Min(layerOffsets), off.Sum(count*8)), nil -} diff --git a/tools/sotn-assets/layout.go b/tools/sotn-assets/layout.go deleted file mode 100644 index 98f44bc1b..000000000 --- a/tools/sotn-assets/layout.go +++ /dev/null @@ -1,213 +0,0 @@ -package main - -import ( - "encoding/binary" - "fmt" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" - "io" - "os" - "path" - "strconv" - "strings" -) - -type layoutEntry struct { - X int16 `json:"x"` - Y int16 `json:"y"` - ID string `json:"id"` - Flags uint8 `json:"flags"` // TODO properly de-serialize this - Slot uint8 `json:"slot"` - SpawnID uint8 `json:"spawnId"` - Params uint16 `json:"params"` - YOrder *int `json:"yOrder,omitempty"` -} - -type layouts struct { - Entities [][]layoutEntry `json:"entities"` - Indices []int `json:"indices"` -} - -func fetchEntityIDsFromHFile(overlay string) (map[int]string, error) { - // Get the EntityIDs enum from the .h file and invert it to get a lookup table - // Keys are integers, values are the names from the enum. - hFile, err := os.ReadFile("src/st/" + overlay + "/" + overlay + ".h") - if err != nil { - return nil, err - } - lines := strings.Split(string(hFile), "\n") - // Extract all the lines that are part of the enum. - // Do this by searching for the first "EntityIDs" (in typedef enum EntityIDs {) - // and the last "EntityIDs" (in } EntityIDs;) - enumData := []string{} - inEnum := false - for _,line := range lines { - if strings.Contains(line, "EntityIDs"){ - if inEnum{ - break - } else { - inEnum = true - } - } else if inEnum { - enumData = append(enumData, line) - } - } - // Now we have the enum's lines loaded. Iterate through populating a map. - entityNames := make(map[int]string, 255) - // Increments in the enum, updates if enum has a direct assign - index := -1 // start at -1 so first increment takes it to 0 to begin - for _,line := range enumData { - line = strings.Split(line, ",")[0] // go up to the comma - parts := strings.Split(line, " = ") - if len(parts) > 1 { - hexVal := strings.Replace(parts[1], "0x", "", -1) - // Windows nonsense, remove any \r that exists - hexVal = strings.Replace(hexVal, "\r", "", -1) - parsed, err := strconv.ParseInt(hexVal, 16, 16) - if err != nil { - return nil, err - } - index = int(parsed) - } else { - index ++ - } - parts = strings.Split(parts[0], " ") - name := parts[len(parts) - 1] - entityNames[index] = name - } - return entityNames, nil -} - -func readEntityLayoutEntry(file *os.File) (layoutEntry, error) { - ovlName := strings.ToLower(path.Base(path.Dir(file.Name()))) - entityIDs, _ := fetchEntityIDsFromHFile(ovlName) - - bs := make([]byte, 10) - if _, err := io.ReadFull(file, bs); err != nil { - return layoutEntry{}, err - } - - var entityIDStr string - id := int(bs[4]) - // Try to load the proper enum - entityIDStr = entityIDs[id] - // If enum unknown or flags are set, override, don't use enums - if entityIDStr == "" || bs[5] != 0 { - entityIDStr = fmt.Sprintf("0x%02X", id) - } - - return layoutEntry{ - X: int16(binary.LittleEndian.Uint16(bs[0:2])), - Y: int16(binary.LittleEndian.Uint16(bs[2:4])), - ID: entityIDStr, - Flags: bs[5], - Slot: bs[6], - SpawnID: bs[7], - Params: binary.LittleEndian.Uint16(bs[8:10]), - }, nil -} - -// the Y-ordered entries list has a different order than the X-ordered one. The order cannot consistently get -// restored by just sorting entries by Y as usually entries with the same Y results swapped. -// This algorithm will fill the optional field YOrder, only useful to restore the original order. -func hydrateYOrderFields(x layouts, y layouts) error { - if len(x.Indices) != len(y.Indices) { - return fmt.Errorf("number of X and Y layout indices do not match") - } - if len(x.Entities) != len(y.Entities) { - return fmt.Errorf("number of X and Y layout entries do not match") - } - - populateYOrderField := func(xEntries []layoutEntry, yEntries []layoutEntry) { - yIndexMap := make(map[layoutEntry]int, len(yEntries)) - for i, e := range yEntries { - yIndexMap[e] = i - } - for i := 0; i < len(xEntries); i++ { - if yOrder, found := yIndexMap[xEntries[i]]; found { - xEntries[i].YOrder = &yOrder - } - } - } - - for i := 0; i < len(x.Entities); i++ { - xList := x.Entities[i] - yList := y.Entities[i] - if len(xList) != len(yList) { - return fmt.Errorf("number of X and Y entries do not match") - } - populateYOrderField(xList, yList) - } - return nil -} - -func readEntityLayout(file *os.File, off psx.Addr, count int, isX bool) (layouts, []datarange.DataRange, error) { - if err := off.MoveFile(file, psx.RamStageBegin); err != nil { - return layouts{}, nil, err - } - - // there are two copies of the layout, one ordered by X and the other one ordered by Y - // we will only read the first one, which is ordered by Y - blockOffsets := make([]psx.Addr, count) - if err := binary.Read(file, binary.LittleEndian, blockOffsets); err != nil { - return layouts{}, nil, err - } - - // the order of each layout entry must be preserved - pool := map[psx.Addr]int{} - blocks := [][]layoutEntry{} - xRanges := []datarange.DataRange{} - for _, blockOffset := range sortUniqueOffsets(blockOffsets) { - if err := blockOffset.MoveFile(file, psx.RamStageBegin); err != nil { - return layouts{}, nil, err - } - entries := []layoutEntry{} - for { - entry, err := readEntityLayoutEntry(file) - if err != nil { - return layouts{}, nil, err - } - if entry.X == -1 && entry.Y == -1 { - entries = append(entries, entry) - break - } - entries = append(entries, entry) - } - - // sanity check on the first entry - if entries[0].X != -2 || entries[0].Y != -2 { - err := fmt.Errorf("first layout entry does not mark the beginning of the array: %v", entries[0]) - return layouts{}, nil, err - } - - pool[blockOffset] = len(blocks) - blocks = append(blocks, entries) - xRanges = append(xRanges, datarange.FromAddr(blockOffset, len(entries)*10)) - } - // the very last entry needs to be aligned by 4 - xRanges[len(xRanges)-1] = xRanges[len(xRanges)-1].Align4() - - l := layouts{Entities: blocks} - for _, blockOffset := range blockOffsets { - l.Indices = append(l.Indices, pool[blockOffset]) - } - - endOfArray := off.Sum(count * 4) - if isX { // we want to do the same thing with the vertically aligned layout - yLayouts, yRanges, err := readEntityLayout(file, endOfArray, count, false) - if err != nil { - return layouts{}, nil, fmt.Errorf("readEntityLayout failed on Y: %w", err) - } - if err := hydrateYOrderFields(l, yLayouts); err != nil { - return layouts{}, nil, fmt.Errorf("unable to populate YOrder field: %w", err) - } - xMerged := datarange.MergeDataRanges(xRanges) - yMerged := yRanges[1] - return l, []datarange.DataRange{ - datarange.MergeDataRanges([]datarange.DataRange{datarange.New(off, endOfArray), yRanges[0]}), - datarange.MergeDataRanges([]datarange.DataRange{xMerged, yMerged}), - }, nil - } else { - return l, []datarange.DataRange{datarange.New(off, endOfArray), datarange.MergeDataRanges(xRanges)}, nil - } -} diff --git a/tools/sotn-assets/main.go b/tools/sotn-assets/main.go index 32cc1ff65..2d6e6c8eb 100644 --- a/tools/sotn-assets/main.go +++ b/tools/sotn-assets/main.go @@ -1,348 +1,15 @@ package main import ( - "encoding/binary" - "encoding/json" - "flag" "fmt" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/rooms" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/assets/spritebanks" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/datarange" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/util" "os" - "path" ) -type dataContainer[T any] struct { - dataRange datarange.DataRange - content T -} - -type ovl struct { - ranges []datarange.DataRange - layers dataContainer[[]roomLayers] - graphics dataContainer[gfx] - layouts dataContainer[layouts] - layoutsExtraRange datarange.DataRange - tileMaps dataContainer[map[psx.Addr][]byte] - tileDefs dataContainer[map[psx.Addr]tileDef] -} - -type stageHeader struct { - FnUpdate psx.Addr - FnHitDetection psx.Addr - FnUpdateRoomPos psx.Addr - FnInitRoomEntities psx.Addr - Rooms psx.Addr // ✅ - Sprites psx.Addr // ✅ - Cluts psx.Addr // 🫥 - Layouts psx.Addr // ✅ - Layers psx.Addr // ✅ - Graphics psx.Addr // 🫥 WIP - FnUpdateStageEntities psx.Addr -} - -func getStageHeader(fileName string) (stageHeader, error) { - file, err := os.Open(fileName) - if err != nil { - return stageHeader{}, fmt.Errorf("failed to read stage header: %w", err) - } - defer file.Close() - - var header stageHeader - if err := binary.Read(file, binary.LittleEndian, &header); err != nil { - return stageHeader{}, fmt.Errorf("failed to read stage header: %w", err) - } - return header, nil -} - -func getOvlAssets(fileName string) (ovl, error) { - header, err := getStageHeader(fileName) - if err != nil { - return ovl{}, fmt.Errorf("failed to get ovl assets: %w", err) - } - - file, err := os.Open(fileName) - if err != nil { - return ovl{}, err - } - defer file.Close() - - _, roomsRange, err := rooms.ReadRooms(file, header.Rooms) - if err != nil { - return ovl{}, fmt.Errorf("unable to read rooms: %w", err) - } - - layers, layersRange, err := readLayers(file, header.Layers) - if err != nil { - return ovl{}, fmt.Errorf("unable to read layers: %w", err) - } - - tileMaps, tileMapsRange, err := readAllTileMaps(file, layers) - if err != nil { - return ovl{}, fmt.Errorf("unable to gather all the tile maps: %w", err) - } - - tileDefs, tileDefsRange, err := readAllTiledefs(file, layers) - if err != nil { - return ovl{}, fmt.Errorf("unable to gather all the tile defs: %w", err) - } - - // check for unused tile defs (CEN has one) - for tileMapsRange.End() < tileDefsRange.Begin() { - offset := tileDefsRange.Begin().Sum(-0x10) - unusedTileDef, unusedTileDefRange, err := readTiledef(file, offset) - if err != nil { - return ovl{}, fmt.Errorf("there is a gap between tileMaps and tileDefs: %w", err) - } - tileDefs[offset] = unusedTileDef - tileDefsRange = datarange.MergeDataRanges([]datarange.DataRange{tileDefsRange, unusedTileDefRange}) - } - - _, spritesRange, err := spritebanks.ReadSpritesBanks(file, psx.RamStageBegin, header.Sprites) - if err != nil { - return ovl{}, fmt.Errorf("unable to gather all sprites: %w", err) - } - - graphics, graphicsRange, err := readGraphics(file, header.Graphics) - if err != nil { - return ovl{}, fmt.Errorf("unable to gather all graphics: %w", err) - } - - layoutOff := header.Layouts - if layoutOff == psx.RamNull { - // some overlays have this field nulled, we have to find the offset ourselves - // it should be usually be right after header.Graphics - layoutOff = graphicsRange.End() // ⚠️ assumption - } - nLayouts := 53 // it seems there are always 53 elements?! - entityLayouts, layoutsRange, err := readEntityLayout(file, layoutOff, nLayouts, true) - if err != nil { - return ovl{}, fmt.Errorf("unable to gather all entity layouts: %w", err) - } - - return ovl{ - ranges: datarange.ConsolidateDataRanges([]datarange.DataRange{ - roomsRange, - layersRange, - spritesRange, - graphicsRange, - layoutsRange[0], - layoutsRange[1], - tileMapsRange, - tileDefsRange, - }), - layers: dataContainer[[]roomLayers]{dataRange: layersRange, content: layers}, - graphics: dataContainer[gfx]{dataRange: graphicsRange, content: graphics}, - layouts: dataContainer[layouts]{dataRange: layoutsRange[1], content: entityLayouts}, - layoutsExtraRange: layoutsRange[0], - tileMaps: dataContainer[map[psx.Addr][]byte]{dataRange: tileMapsRange, content: tileMaps}, - tileDefs: dataContainer[map[psx.Addr]tileDef]{dataRange: tileDefsRange, content: tileDefs}, - }, nil -} - -func extractOvlAssets(o ovl, outputDir string) error { - if err := os.MkdirAll(outputDir, 0755); err != nil { - return err - } - - content, err := json.MarshalIndent(o.layers.content, "", " ") - if err != nil { - return err - } - if err := os.WriteFile(path.Join(outputDir, "layers.json"), content, 0644); err != nil { - return fmt.Errorf("unable to create layers file: %w", err) - } - - content, err = json.MarshalIndent(o.layouts.content, "", " ") - if err != nil { - return err - } - if err := os.WriteFile(path.Join(outputDir, "entity_layouts.json"), content, 0644); err != nil { - return fmt.Errorf("unable to create entity layouts file: %w", err) - } - - for offset, bytes := range o.tileMaps.content { - fileName := path.Join(outputDir, getTilemapFileName(offset)) - if err := os.WriteFile(fileName, bytes, 0644); err != nil { - return fmt.Errorf("unable to create %q: %w", fileName, err) - } - } - - for offset, tileDefsData := range o.tileDefs.content { - defs := tileDefPaths{ - Tiles: getTiledefIndicesFileName(offset), - Pages: getTiledefPagesFileName(offset), - Cluts: getTiledefClutsFileName(offset), - Collisions: getTiledefCollisionsFileName(offset), - } - if err := os.WriteFile(path.Join(outputDir, defs.Tiles), tileDefsData.tiles, 0644); err != nil { - return fmt.Errorf("unable to create %q: %w", defs.Tiles, err) - } - if err := os.WriteFile(path.Join(outputDir, defs.Pages), tileDefsData.pages, 0644); err != nil { - return fmt.Errorf("unable to create %q: %w", defs.Pages, err) - } - if err := os.WriteFile(path.Join(outputDir, defs.Cluts), tileDefsData.cluts, 0644); err != nil { - return fmt.Errorf("unable to create %q: %w", defs.Cluts, err) - } - if err := os.WriteFile(path.Join(outputDir, defs.Collisions), tileDefsData.cols, 0644); err != nil { - return fmt.Errorf("unable to create %q: %w", defs.Collisions, err) - } - - content, err = json.MarshalIndent(defs, "", " ") - if err != nil { - return err - } - fileName := path.Join(outputDir, getTiledefFileName(offset)) - if err := os.WriteFile(fileName, content, 0644); err != nil { - return fmt.Errorf("unable to create %q: %w", fileName, err) - } - } - - return nil -} - -func extract(fileName string, outputDir string) error { - o, err := getOvlAssets(fileName) - if err != nil { - return fmt.Errorf("unable to retrieve OVL assets: %w", err) - } - - err = extractOvlAssets(o, outputDir) - if err != nil { - return fmt.Errorf("unable to extract OVL assets: %w", err) - } - - return nil -} - -func info(fileName string) error { - stHeader, err := getStageHeader(fileName) - if err != nil { - return fmt.Errorf("unable to retrieve stage info: %w", err) - } - fmt.Println("asset config hints:") - fmt.Printf(" - [0x%X, sprite_banks]\n", stHeader.Sprites.Real(psx.RamStageBegin)) - fmt.Printf(" - [0x%X, skip]\n", stHeader.Sprites.Sum(24*4).Real(psx.RamStageBegin)) - - o, err := getOvlAssets(fileName) - if err != nil { - return fmt.Errorf("unable to retrieve OVL assets: %w", err) - } - - entries := []struct { - dataRange datarange.DataRange - name string - comment string - }{ - {o.layers.dataRange, "header", "layers"}, - {o.layoutsExtraRange, "e_laydef", "layout entries header"}, - {o.layouts.dataRange, "e_layout", "layout entries data"}, - {o.tileMaps.dataRange, "tile_data", "tile data"}, - {o.tileDefs.dataRange, "tile_data", "tile definitions"}, - } - - fmt.Printf("data coverage: %+v\n", o.ranges) - fmt.Println("subsegment hints:") - fmt.Println(" - [0x0, .data, header]") - for i := 0; i < len(entries); i++ { - e := entries[i] - s := fmt.Sprintf(" - [0x%X, .data, %s]", e.dataRange.Begin().Real(psx.RamStageBegin), e.name) - if e.comment != "" { - s = fmt.Sprintf("%s # %s", s, e.comment) - } - fmt.Println(s) - - // if there is a gap between the current entry and the next one, mark it as unrecognized data - if i == len(entries)-1 || e.dataRange.End() != entries[i+1].dataRange.Begin() { - fmt.Printf(" - [0x%X, data]\n", e.dataRange.End().Real(psx.RamStageBegin)) - } - } - return nil -} - -func testStuff() { - _ = []string{ - "ARE", "CAT", "CEN", "CHI", "DAI", "DRE", "LIB", "MAD", - "NO0", "NO1", "NO2", "NO3", "NO4", "NP3", "NZ0", "NZ1", - "ST0", "TE1", "TE2", "TE3", "TE4", "TE5", "TOP", "WRP", - "RARE", "RCAT", "RCEN", "RCHI", "RDAI", "RLIB", "RNO0", "RNO1", - "RNO2", "RNO3", "RNO4", "RNZ0", "RNZ1", "RTOP", "RWRP"} - //ovls := []string{ - // /*"ARE",*/ "CAT", "CEN", "CHI" /*"DAI",*/, "DRE", "LIB", /*"MAD",*/ - // /*"NO0",*/ "NO1", "NO2", "NO3" /*"NO4",*/, "NP3", "NZ0", "NZ1", - // "ST0", "TE1", "TE2", "TE3", "TE4", "TE5" /*"TOP",*/, "WRP", - // "RARE", "RCAT" /*"RCEN",*/, "RCHI" /*"RDAI",*/ /*"RLIB",*/ /*"RNO0",*/, "RNO1", - // /*"RNO2",*/ "RNO3" /*"RNO4",*/ /*"RNZ0",*/ /*"RNZ1",*/ /*"RTOP",*/, "RWRP"} - ovls := []string{"NZ0"} - - for _, ovl := range ovls { - fmt.Printf("processing %s...\n", ovl) - fileName := fmt.Sprintf("../../disks/us/ST/%s/%s.BIN", ovl, ovl) - if err := extract(fileName, "sample/"+ovl); err != nil { - panic(err) - } - } - - if err := buildAll("sample/NZ0", "buildAll/nz0"); err != nil { - panic(err) - } -} - -func handlerStage(args []string) error { - commands := map[string]func(args []string) error{} - commands["info"] = func(args []string) error { - var stageOvl string - extractCmd := flag.NewFlagSet("info", flag.ExitOnError) - extractCmd.StringVar(&stageOvl, "stage_ovl", "", "The overlay file to process") - extractCmd.Parse(args) - return info(stageOvl) - } - commands["extract"] = func(args []string) error { - var stageOvl string - var assetDir string - extractCmd := flag.NewFlagSet("extract", flag.ExitOnError) - extractCmd.StringVar(&stageOvl, "stage_ovl", "", "The overlay file to process") - extractCmd.StringVar(&assetDir, "o", "", "Where to extract the asset files") - extractCmd.Parse(args) - - if stageOvl == "" || assetDir == "" { - fmt.Fprintln(os.Stderr, "stage_ovl and asset_dir are required for extract") - extractCmd.PrintDefaults() - os.Exit(1) - } - return extract(stageOvl, assetDir) - } - commands["build_all"] = func(args []string) error { - buildCmd := flag.NewFlagSet("build_all", flag.ExitOnError) - var inputDir string - var outputDir string - buildCmd.StringVar(&inputDir, "i", "", "Folder where all the assets are located") - buildCmd.StringVar(&outputDir, "o", "", "Where to store the processed source files") - buildCmd.Parse(args) - - if inputDir == "" || outputDir == "" { - fmt.Fprintln(os.Stderr, "input_dir and output_dir are required for build") - buildCmd.PrintDefaults() - os.Exit(1) - } - return buildAll(inputDir, outputDir) - } - - if len(args) > 0 { - command := args[0] - if f, found := commands[command]; found { - return f(args[1:]) - } - fmt.Fprintf(os.Stderr, "unknown subcommand %q. Valid subcommands are %s\n", command, joinMapKeys(commands, ", ")) - } else { - fmt.Fprintf(os.Stderr, "Need a subcommand. Valid subcommands are %s\n", joinMapKeys(commands, ", ")) - } - os.Exit(1) - return nil -} - func handlerConfigExtract(args []string) error { + if len(args) != 1 { + return fmt.Errorf("usage: sotn-assets extract ") + } c, err := readConfig(args[0]) if err != nil { return err @@ -351,6 +18,9 @@ func handlerConfigExtract(args []string) error { } func handlerConfigBuild(args []string) error { + if len(args) != 1 { + return fmt.Errorf("usage: sotn-assets build ") + } c, err := readConfig(args[0]) if err != nil { return err @@ -358,33 +28,18 @@ func handlerConfigBuild(args []string) error { return buildFromConfig(c) } -func handlerConfig(args []string) error { - commands := map[string]func(args []string) error{ - "extract": handlerConfigExtract, - "build": handlerConfigBuild, +func handlerInfo(args []string) error { + if len(args) != 1 { + return fmt.Errorf("usage: sotn-assets info ") } - - if len(args) > 0 { - command := args[0] - if f, found := commands[command]; found { - if err := f(args[1:]); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - return nil - } - fmt.Fprintf(os.Stderr, "unknown subcommand %q. Valid subcommand are %s\n", command, joinMapKeys(commands, ", ")) - } else { - fmt.Fprintf(os.Stderr, "Need a subcommand. Valid subcommand are %s\n", joinMapKeys(commands, ", ")) - } - os.Exit(1) - return nil + return info(os.Stdout, args[0]) } func main() { commands := map[string]func(args []string) error{ - "stage": handlerStage, - "config": handlerConfig, + "extract": handlerConfigExtract, + "build": handlerConfigBuild, + "info": handlerInfo, } args := os.Args[1:] @@ -392,14 +47,14 @@ func main() { command := args[0] if f, found := commands[command]; found { if err := f(args[1:]); err != nil { - fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } return } - fmt.Fprintf(os.Stderr, "unknown command %q. Valid commands are %s\n", command, joinMapKeys(commands, ", ")) + fmt.Fprintf(os.Stderr, "unknown command %q. Valid commands are %s\n", command, util.JoinMapKeys(commands, ", ")) } else { - fmt.Fprintf(os.Stderr, "Need a command. Valid commands are %s\n", joinMapKeys(commands, ", ")) + fmt.Fprintf(os.Stderr, "Need a command. Valid commands are %s\n", util.JoinMapKeys(commands, ", ")) } os.Exit(1) } diff --git a/tools/sotn-assets/paths.go b/tools/sotn-assets/paths.go deleted file mode 100644 index 41f77a5f4..000000000 --- a/tools/sotn-assets/paths.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "fmt" - "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" -) - -func getTilemapFileName(off psx.Addr) string { - return fmt.Sprintf("tilemap_%05X.bin", off.Real(psx.RamStageBegin)) -} - -func getTiledefFileName(off psx.Addr) string { - return fmt.Sprintf("tiledef_%05X.json", off.Real(psx.RamStageBegin)) -} - -func getTiledefIndicesFileName(off psx.Addr) string { - return fmt.Sprintf("tiledef_%05X_tiles.bin", off.Real(psx.RamStageBegin)) -} - -func getTiledefPagesFileName(off psx.Addr) string { - return fmt.Sprintf("tiledef_%05X_pages.bin", off.Real(psx.RamStageBegin)) -} - -func getTiledefClutsFileName(off psx.Addr) string { - return fmt.Sprintf("tiledef_%05X_cluts.bin", off.Real(psx.RamStageBegin)) -} - -func getTiledefCollisionsFileName(off psx.Addr) string { - return fmt.Sprintf("tiledef_%05X_cols.bin", off.Real(psx.RamStageBegin)) -} diff --git a/tools/sotn-assets/sotn/enum.go b/tools/sotn-assets/sotn/enum.go new file mode 100644 index 000000000..7db618909 --- /dev/null +++ b/tools/sotn-assets/sotn/enum.go @@ -0,0 +1,72 @@ +package sotn + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strconv" + "strings" +) + +func removeComments(line string) string { + for { + start := strings.Index(line, "/*") + end := strings.Index(line, "*/") + if start == -1 || end == -1 || end < start { + break + } + line = line[:start] + line[end+2:] + } + trailingCommentIndex := strings.Index(line, "//") + if trailingCommentIndex != -1 { + line = line[:trailingCommentIndex] + } + return strings.TrimSpace(line) +} + +func ParseCEnum(r io.Reader, name string) (map[int]string, error) { + enumMap := make(map[int]string, 0x100) + for i := 0; i < 0x100; i++ { + enumMap[i] = fmt.Sprintf("0x%02X", i) + } + scanner := bufio.NewScanner(r) + startRegex := regexp.MustCompile(fmt.Sprintf(`enum\s+%s\s*{`, name)) + currentValue := 0 + for scanner.Scan() { + line := removeComments(scanner.Text()) + if startRegex.MatchString(line) { + for scanner.Scan() { + line := removeComments(scanner.Text()) + line = strings.TrimRight(line, ",") + if strings.Contains(line, "}") { + break + } + parts := strings.Split(line, "=") + name := strings.TrimSpace(parts[0]) + if name == "" { + continue + } + if len(parts) > 1 { + valueStr := strings.TrimSpace(parts[1]) + base := 10 + if strings.HasPrefix(valueStr, "0x") { + valueStr = valueStr[2:] + base = 16 + } + value, err := strconv.ParseInt(valueStr, base, 32) + if err != nil { + return nil, err + } + currentValue = int(value) + } + enumMap[currentValue] = name + currentValue++ + } + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + return enumMap, nil +} diff --git a/tools/sotn-assets/sotn/enum_test.go b/tools/sotn-assets/sotn/enum_test.go new file mode 100644 index 000000000..8bdbf3772 --- /dev/null +++ b/tools/sotn-assets/sotn/enum_test.go @@ -0,0 +1,33 @@ +package sotn + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "strings" + "testing" +) + +func TestParseCEnum(t *testing.T) { + src := "" + + "typedef enum IgnoreMe{ ONE, TWO, THREE };\n" + + "// typedef struct MyEnum { this is a comment\n" + + "enum MyEnum { // this is also a comment\n" + + "First,\n" + + "Second,\n" + + "SomeId = 10\n" + + "SomeHexId = 0x10\n" + + "/* 0x18nope */ E_MARIA = 0x18\n" + + "E_COMMENT = 123 // ignore\n" + + "E_COMMENT_2 /* ignore this as well */\n" + + "} // malformed, it misses a semicolon\n" + m, err := ParseCEnum(strings.NewReader(src), "MyEnum") + require.NoError(t, err) + assert.Equal(t, "First", m[0]) + assert.Equal(t, "Second", m[1]) + assert.Equal(t, "0x02", m[2]) + assert.Equal(t, "SomeId", m[10]) + assert.Equal(t, "SomeHexId", m[0x10]) + assert.Equal(t, "E_MARIA", m[0x18]) + assert.Equal(t, "E_COMMENT", m[123]) + assert.Equal(t, "E_COMMENT_2", m[124]) +} diff --git a/tools/sotn-assets/sotn/stage.go b/tools/sotn-assets/sotn/stage.go new file mode 100644 index 000000000..77c86e6ea --- /dev/null +++ b/tools/sotn-assets/sotn/stage.go @@ -0,0 +1,33 @@ +package sotn + +import ( + "encoding/binary" + "fmt" + "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" + "io" +) + +type StageHeader struct { + FnUpdate psx.Addr + FnHitDetection psx.Addr + FnUpdateRoomPos psx.Addr + FnInitRoomEntities psx.Addr + Rooms psx.Addr // ✅ + Sprites psx.Addr // ✅ + Cluts psx.Addr // 🫥 + Layouts psx.Addr // ✅ + Layers psx.Addr // ✅ + Graphics psx.Addr // 🫥 WIP + FnUpdateStageEntities psx.Addr +} + +func ReadStageHeader(r io.ReadSeeker) (StageHeader, error) { + var header StageHeader + if _, err := r.Seek(0, io.SeekStart); err != nil { + return header, fmt.Errorf("failed to seek to stage header: %w", err) + } + if err := binary.Read(r, binary.LittleEndian, &header); err != nil { + return header, fmt.Errorf("failed to read stage header: %w", err) + } + return header, nil +} diff --git a/tools/sotn-assets/utils.go b/tools/sotn-assets/util/utils.go similarity index 77% rename from tools/sotn-assets/utils.go rename to tools/sotn-assets/util/utils.go index 8c8e12f6e..eea1ffefc 100644 --- a/tools/sotn-assets/utils.go +++ b/tools/sotn-assets/util/utils.go @@ -1,4 +1,4 @@ -package main +package util import ( "github.com/xeeynamo/sotn-decomp/tools/sotn-assets/psx" @@ -6,7 +6,7 @@ import ( "strings" ) -func joinMapKeys[T any](m map[string]T, sep string) string { +func JoinMapKeys[T any](m map[string]T, sep string) string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) @@ -14,7 +14,7 @@ func joinMapKeys[T any](m map[string]T, sep string) string { return strings.Join(keys, sep) } -func minBy[T any](slice []T, getter func(T) int) (max int) { +func MinBy[T any](slice []T, getter func(T) int) (max int) { if len(slice) == 0 { return max } @@ -28,7 +28,7 @@ func minBy[T any](slice []T, getter func(T) int) (max int) { return max } -func maxBy[T any](slice []T, getter func(T) int) (max int) { +func MaxBy[T any](slice []T, getter func(T) int) (max int) { if len(slice) == 0 { return max } @@ -42,14 +42,14 @@ func maxBy[T any](slice []T, getter func(T) int) (max int) { return max } -func btoi(b bool) int { +func Btoi(b bool) int { if b { return 1 } return 0 } -func sortUniqueOffsets(slice []psx.Addr) []psx.Addr { +func SortUniqueOffsets(slice []psx.Addr) []psx.Addr { unique := map[psx.Addr]struct{}{} for _, v := range slice { unique[v] = struct{}{}