summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Companion.cpp28
-rw-r--r--src/factories/pm64/AudioFactory.cpp614
-rw-r--r--src/factories/pm64/AudioFactory.h23
-rw-r--r--src/factories/pm64/BackgroundFactory.cpp104
-rw-r--r--src/factories/pm64/BackgroundFactory.h23
-rw-r--r--src/factories/pm64/CollisionFactory.cpp192
-rw-r--r--src/factories/pm64/CollisionFactory.h23
-rw-r--r--src/factories/pm64/EffectDListFactory.cpp235
-rw-r--r--src/factories/pm64/EffectDListFactory.h24
-rw-r--r--src/factories/pm64/EntityGfxFactory.cpp569
-rw-r--r--src/factories/pm64/EntityGfxFactory.h40
-rw-r--r--src/factories/pm64/ImgFXAnimFactory.cpp107
-rw-r--r--src/factories/pm64/ImgFXAnimFactory.h23
-rw-r--r--src/factories/pm64/MapTextureFactory.cpp256
-rw-r--r--src/factories/pm64/MapTextureFactory.h23
-rw-r--r--src/factories/pm64/ShapeFactory.cpp736
-rw-r--r--src/factories/pm64/ShapeFactory.h45
-rw-r--r--src/factories/pm64/SpriteFactory.cpp208
-rw-r--r--src/factories/pm64/SpriteFactory.h23
-rw-r--r--src/factories/pm64/StoryImageFactory.cpp68
-rw-r--r--src/factories/pm64/StoryImageFactory.h23
-rw-r--r--src/factories/pm64/TitleDataFactory.cpp60
-rw-r--r--src/factories/pm64/TitleDataFactory.h23
23 files changed, 3470 insertions, 0 deletions
diff --git a/src/Companion.cpp b/src/Companion.cpp
index 08e1ed2..08022d5 100644
--- a/src/Companion.cpp
+++ b/src/Companion.cpp
@@ -72,6 +72,20 @@
#include "factories/sf64/TriangleFactory.h"
#endif
+#ifdef PM64_SUPPORT
+#include "factories/pm64/SpriteFactory.h"
+#include "factories/pm64/ShapeFactory.h"
+#include "factories/pm64/BackgroundFactory.h"
+#include "factories/pm64/CollisionFactory.h"
+#include "factories/pm64/MapTextureFactory.h"
+#include "factories/pm64/AudioFactory.h"
+#include "factories/pm64/StoryImageFactory.h"
+#include "factories/pm64/ImgFXAnimFactory.h"
+#include "factories/pm64/TitleDataFactory.h"
+#include "factories/pm64/EntityGfxFactory.h"
+#include "factories/pm64/EffectDListFactory.h"
+#endif
+
#ifdef FZERO_SUPPORT
#include "factories/fzerox/EADAnimationFactory.h"
#include "factories/fzerox/CourseFactory.h"
@@ -186,6 +200,20 @@ void Companion::Init(const ExportType type, std::atomic<size_t>& assetCount) {
this->RegisterFactory("MK64:PACKED_GFX", std::make_shared<MK64::PackedDListFactory>());
#endif
+#ifdef PM64_SUPPORT
+ this->RegisterFactory("PM64:SPRITE", std::make_shared<PM64SpriteFactory>());
+ this->RegisterFactory("PM64:SHAPE", std::make_shared<PM64ShapeFactory>());
+ this->RegisterFactory("PM64:BACKGROUND", std::make_shared<PM64BackgroundFactory>());
+ this->RegisterFactory("PM64:COLLISION", std::make_shared<PM64CollisionFactory>());
+ this->RegisterFactory("PM64:MAP_TEXTURE", std::make_shared<PM64MapTextureFactory>());
+ this->RegisterFactory("PM64:AUDIO", std::make_shared<PM64AudioFactory>());
+ this->RegisterFactory("PM64:STORY_IMAGE", std::make_shared<PM64StoryImageFactory>());
+ this->RegisterFactory("PM64:IMGFX_ANIM", std::make_shared<PM64ImgFXAnimFactory>());
+ this->RegisterFactory("PM64:TITLE_DATA", std::make_shared<PM64TitleDataFactory>());
+ this->RegisterFactory("PM64:ENTITY_GFX", std::make_shared<PM64EntityGfxFactory>());
+ this->RegisterFactory("PM64:EFFECT_DL", std::make_shared<PM64EffectDListFactory>());
+#endif
+
#ifdef SF64_SUPPORT
this->RegisterFactory("SF64:ANIM", std::make_shared<SF64::AnimFactory>());
this->RegisterFactory("SF64:SKELETON", std::make_shared<SF64::SkeletonFactory>());
diff --git a/src/factories/pm64/AudioFactory.cpp b/src/factories/pm64/AudioFactory.cpp
new file mode 100644
index 0000000..a4df6bf
--- /dev/null
+++ b/src/factories/pm64/AudioFactory.cpp
@@ -0,0 +1,614 @@
+#include "AudioFactory.h"
+#include "Companion.h"
+#include "spdlog/spdlog.h"
+#include <set>
+
+// SBN file format constants
+#define SBN_SIGNATURE 0x53424E20 // 'SBN '
+#define INIT_SIGNATURE 0x494E4954 // 'INIT'
+#define BGM_SIGNATURE 0x42474D20 // 'BGM '
+#define SEF_SIGNATURE 0x53454620 // 'SEF '
+#define PER_SIGNATURE 0x50455220 // 'PER '
+#define PRG_SIGNATURE 0x50524720 // 'PRG '
+#define BK_SIGNATURE 0x424B // 'BK'
+#define MSEQ_SIGNATURE 0x4D534551 // 'MSEQ'
+
+// Audio file format types (upper byte of SBNFileEntry.data)
+// NOTE: PER and PRG share format 0x40 with MSEQ; distinguished by file signature
+#define AU_FMT_BGM 0x10
+#define AU_FMT_SEF 0x20
+#define AU_FMT_BK 0x30
+#define AU_FMT_MSEQ 0x40
+
+// Structure sizes
+#define SBN_HEADER_SIZE 0x40
+#define SBN_FILE_ENTRY_SIZE 8
+#define INIT_HEADER_SIZE 0x20
+#define INIT_SONG_ENTRY_SIZE 8
+#define INIT_BANK_ENTRY_SIZE 4
+#define BGM_HEADER_SIZE 0x24
+#define BK_HEADER_SIZE 0x40
+#define SEF_HEADER_SIZE 0x22
+#define MSEQ_HEADER_SIZE 0x18
+#define PER_HEADER_SIZE 0x10
+
+// SEF section entry counts (from game code)
+#define SEF_SECTION_0_3_ENTRIES 0xC0 // 192 entries for sections 0-3
+#define SEF_SECTION_4_7_ENTRIES 0x40 // 64 entries for sections 4-7
+#define SEF_EXTRA_ENTRIES 0x140 // 320 entries for extra section
+
+// BGMDrumInfo size
+#define BGM_DRUM_INFO_SIZE 0x0C
+// BGMInstrumentInfo size
+#define BGM_INSTRUMENT_INFO_SIZE 0x08
+// PEREntry size (12 drums)
+#define PER_ENTRY_SIZE (12 * BGM_DRUM_INFO_SIZE) // 0x90
+
+// Helper to check bounds
+#define CHECK_BOUNDS(offset, size, totalSize) ((offset) + (size) <= (totalSize))
+
+static void ByteSwapAudioData(uint8_t* data, size_t size) {
+ if (size < SBN_HEADER_SIZE) {
+ SPDLOG_WARN("Audio data too small for SBN header: {}", size);
+ return;
+ }
+
+ // === SBN Header ===
+ // Offsets: 0x00 signature (s32), 0x04 size (s32), 0x10 fileListOffset (s32),
+ // 0x14 numEntries (s32), 0x18 fullFileSize (s32), 0x1C versionOffset (s32),
+ // 0x24 INIToffset (s32)
+ uint32_t* header32 = reinterpret_cast<uint32_t*>(data);
+
+ // Byte-swap the header fields we need
+ uint32_t signature = BSWAP32(header32[0]);
+ header32[0] = signature;
+ header32[1] = BSWAP32(header32[1]); // size
+
+ uint32_t fileListOffset = BSWAP32(header32[4]); // 0x10
+ uint32_t numEntries = BSWAP32(header32[5]); // 0x14
+ uint32_t fullFileSize = BSWAP32(header32[6]); // 0x18
+ uint32_t versionOffset = BSWAP32(header32[7]); // 0x1C
+ uint32_t initOffset = BSWAP32(header32[9]); // 0x24
+
+ header32[4] = fileListOffset;
+ header32[5] = numEntries;
+ header32[6] = fullFileSize;
+ header32[7] = versionOffset;
+ header32[9] = initOffset;
+
+ SPDLOG_DEBUG("SBN: signature=0x{:08X}, fileListOffset=0x{:X}, numEntries={}, initOffset=0x{:X}",
+ signature, fileListOffset, numEntries, initOffset);
+
+ if (signature != SBN_SIGNATURE) {
+ SPDLOG_ERROR("Invalid SBN signature: 0x{:08X}", signature);
+ return;
+ }
+
+ // === SBN File Entry Array ===
+ // Each entry: s32 offset, u32 data
+ if (CHECK_BOUNDS(fileListOffset, numEntries * SBN_FILE_ENTRY_SIZE, size)) {
+ uint32_t* entries = reinterpret_cast<uint32_t*>(data + fileListOffset);
+ for (uint32_t i = 0; i < numEntries; i++) {
+ uint32_t offset = BSWAP32(entries[i * 2]);
+ uint32_t entryData = BSWAP32(entries[i * 2 + 1]);
+ entries[i * 2] = offset;
+ entries[i * 2 + 1] = entryData;
+
+ // Stop if we hit an invalid entry
+ if ((offset & 0xFFFFFF) == 0) {
+ break;
+ }
+
+ // Get the file type and offset within SBN
+ uint8_t fileType = (entryData >> 24) & 0xFF;
+ uint32_t fileOffset = offset & 0xFFFFFF;
+ uint32_t fileSize = entryData & 0xFFFFFF;
+
+ // Byte-swap the embedded file based on type
+ if (fileOffset > 0 && CHECK_BOUNDS(fileOffset, 8, size)) {
+ uint8_t* fileData = data + fileOffset;
+
+ switch (fileType) {
+ case AU_FMT_BGM: {
+ // BGM Header: s32 signature, s32 size, s32 name, pad[4], BGMFileInfo
+ if (CHECK_BOUNDS(fileOffset, BGM_HEADER_SIZE, size)) {
+ uint32_t* bgm32 = reinterpret_cast<uint32_t*>(fileData);
+ bgm32[0] = BSWAP32(bgm32[0]); // signature
+ uint32_t bgmFileSize = BSWAP32(bgm32[1]);
+ bgm32[1] = bgmFileSize; // size
+ bgm32[2] = BSWAP32(bgm32[2]); // name
+ // pad at 0x0C
+
+ // BGMFileInfo at offset 0x10:
+ // u8 timingPreset, pad[3], u16 compositions[4], u16 drums, u16 drumCount, u16 instruments, u16 instrumentCount
+ uint16_t* bgm16 = reinterpret_cast<uint16_t*>(fileData + 0x14);
+ bgm16[0] = BSWAP16(bgm16[0]); // compositions[0]
+ bgm16[1] = BSWAP16(bgm16[1]); // compositions[1]
+ bgm16[2] = BSWAP16(bgm16[2]); // compositions[2]
+ bgm16[3] = BSWAP16(bgm16[3]); // compositions[3]
+ bgm16[4] = BSWAP16(bgm16[4]); // drums
+ bgm16[5] = BSWAP16(bgm16[5]); // drumCount
+ bgm16[6] = BSWAP16(bgm16[6]); // instruments
+ bgm16[7] = BSWAP16(bgm16[7]); // instrumentCount
+
+ // Swap composition data (SegData/u32 command arrays) and phrase track entries
+ // The BGM player reads these as u32 via SegData* pointers
+ std::set<uint32_t> swappedPhrases;
+ for (int comp = 0; comp < 4; comp++) {
+ uint16_t compOff = bgm16[comp]; // already swapped
+ if (compOff == 0) continue;
+
+ uint32_t compAbsOff = fileOffset + compOff * 4;
+ uint32_t* cmdPtr = reinterpret_cast<uint32_t*>(data + compAbsOff);
+
+ // Walk composition commands until BGM_COMP_END (0x00000000)
+ while (compAbsOff + 4 <= size) {
+ uint32_t raw = *cmdPtr;
+ if (raw == 0) break; // BGM_COMP_END is 0 in both endiannesses
+ uint32_t swapped = BSWAP32(raw);
+ *cmdPtr = swapped;
+
+ // Check for PLAY_PHRASE command (top nibble = 1)
+ if (((swapped >> 28) & 0xF) == 1) {
+ // Phrase offset is relative to compStartPos, in u32 units
+ uint16_t phraseRelOff = swapped & 0xFFFF;
+ uint32_t phraseAbsOff = fileOffset + compOff * 4 + phraseRelOff * 4;
+ if (swappedPhrases.find(phraseAbsOff) == swappedPhrases.end()
+ && CHECK_BOUNDS(phraseAbsOff, 16 * 4, size)) {
+ swappedPhrases.insert(phraseAbsOff);
+ // Swap 16 u32 track info entries
+ uint32_t* phrasePtr = reinterpret_cast<uint32_t*>(data + phraseAbsOff);
+ for (int t = 0; t < 16; t++) {
+ phrasePtr[t] = BSWAP32(phrasePtr[t]);
+ }
+ }
+ }
+ cmdPtr++;
+ compAbsOff += 4;
+ }
+ }
+
+ // Swap BGMDrumInfo entries (u16 bankPatch at +0, u16 keyBase at +2)
+ uint16_t drumsOff = bgm16[4];
+ uint16_t drumCount = bgm16[5];
+ if (drumsOff != 0 && drumCount > 0) {
+ uint32_t drumsAbsOff = fileOffset + drumsOff * 4;
+ for (uint16_t d = 0; d < drumCount; d++) {
+ uint32_t drumEntryOff = drumsAbsOff + d * BGM_DRUM_INFO_SIZE;
+ if (CHECK_BOUNDS(drumEntryOff, BGM_DRUM_INFO_SIZE, size)) {
+ uint16_t* drum16 = reinterpret_cast<uint16_t*>(data + drumEntryOff);
+ drum16[0] = BSWAP16(drum16[0]); // bankPatch
+ drum16[1] = BSWAP16(drum16[1]); // keyBase
+ }
+ }
+ }
+
+ // Swap BGMInstrumentInfo entries (u16 bankPatch at +0)
+ uint16_t instrOff = bgm16[6];
+ uint16_t instrCount = bgm16[7];
+ if (instrOff != 0 && instrCount > 0) {
+ uint32_t instrAbsOff = fileOffset + instrOff * 4;
+ for (uint16_t ins = 0; ins < instrCount; ins++) {
+ uint32_t instrEntryOff = instrAbsOff + ins * BGM_INSTRUMENT_INFO_SIZE;
+ if (CHECK_BOUNDS(instrEntryOff, BGM_INSTRUMENT_INFO_SIZE, size)) {
+ uint16_t* instr16 = reinterpret_cast<uint16_t*>(data + instrEntryOff);
+ instr16[0] = BSWAP16(instr16[0]); // bankPatch
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case AU_FMT_BK: {
+ // BK Header: u16 signature, pad[2], s32 size, s32 name, u16 format, u8 swizzled, pad[3],
+ // u16 instruments[16], u16 instrumentsLength, u16 loopStatesStart, u16 loopStatesLength,
+ // u16 predictorsStart, u16 predictorsLength, u16 envelopesStart, u16 envelopesLength
+ if (CHECK_BOUNDS(fileOffset, BK_HEADER_SIZE, size)) {
+ uint16_t* bk16 = reinterpret_cast<uint16_t*>(fileData);
+ bk16[0] = BSWAP16(bk16[0]); // signature
+ // pad at 0x02
+
+ uint32_t* bk32 = reinterpret_cast<uint32_t*>(fileData + 0x04);
+ uint32_t bkSize = BSWAP32(bk32[0]); // size
+ bk32[0] = bkSize;
+ bk32[1] = BSWAP32(bk32[1]); // name
+
+ bk16 = reinterpret_cast<uint16_t*>(fileData + 0x0C);
+ bk16[0] = BSWAP16(bk16[0]); // format
+ // swizzled (u8) and pad at 0x0E-0x11
+
+ // instruments[16] at 0x12 - swap and save offsets for instrument data swapping
+ uint16_t instrumentOffsets[16];
+ bk16 = reinterpret_cast<uint16_t*>(fileData + 0x12);
+ for (int j = 0; j < 16; j++) {
+ instrumentOffsets[j] = BSWAP16(bk16[j]);
+ bk16[j] = instrumentOffsets[j];
+ }
+
+ // More u16 fields at 0x32
+ bk16 = reinterpret_cast<uint16_t*>(fileData + 0x32);
+ bk16[0] = BSWAP16(bk16[0]); // instrumentsLength
+ bk16[1] = BSWAP16(bk16[1]); // loopStatesStart
+ bk16[2] = BSWAP16(bk16[2]); // loopStatesLength
+ bk16[3] = BSWAP16(bk16[3]); // predictorsStart
+ bk16[4] = BSWAP16(bk16[4]); // predictorsLength
+ bk16[5] = BSWAP16(bk16[5]); // envelopesStart
+ bk16[6] = BSWAP16(bk16[6]); // envelopesLength
+
+ // Save region offsets/lengths for data swapping below
+ uint16_t loopStatesStart = bk16[1];
+ uint16_t loopStatesLength = bk16[2];
+ uint16_t predictorsStart = bk16[3];
+ uint16_t predictorsLength = bk16[4];
+
+ // Now swap each Instrument structure within the BK file
+ // Instrument structure (0x30 bytes):
+ // 0x00: u32 wavData (offset)
+ // 0x04: u32 wavDataLength
+ // 0x08: u32 loopState (offset)
+ // 0x0C: s32 loopStart
+ // 0x10: s32 loopEnd
+ // 0x14: s32 loopCount
+ // 0x18: u32 predictor (offset)
+ // 0x1C: u16 codebookSize
+ // 0x1E: u16 keyBase
+ // 0x20: s32 sampleRate
+ // 0x24-0x2B: u8 fields (no swap needed)
+ // 0x2C: u32 envelopes (offset)
+
+ // Track which envelope presets we've already swapped (multiple instruments may share one)
+ std::set<uint32_t> swappedEnvelopes;
+
+ for (int j = 0; j < 16; j++) {
+ uint16_t instOffset = instrumentOffsets[j];
+ if (instOffset != 0 && CHECK_BOUNDS(fileOffset + instOffset, 0x30, size)) {
+ uint8_t* instData = fileData + instOffset;
+ uint32_t* inst32 = reinterpret_cast<uint32_t*>(instData);
+
+ inst32[0] = BSWAP32(inst32[0]); // wavData
+ inst32[1] = BSWAP32(inst32[1]); // wavDataLength
+ inst32[2] = BSWAP32(inst32[2]); // loopState
+ inst32[3] = BSWAP32(inst32[3]); // loopStart
+ inst32[4] = BSWAP32(inst32[4]); // loopEnd
+ inst32[5] = BSWAP32(inst32[5]); // loopCount
+ inst32[6] = BSWAP32(inst32[6]); // predictor
+
+ uint16_t* inst16 = reinterpret_cast<uint16_t*>(instData + 0x1C);
+ inst16[0] = BSWAP16(inst16[0]); // codebookSize
+ inst16[1] = BSWAP16(inst16[1]); // keyBase
+
+ inst32 = reinterpret_cast<uint32_t*>(instData + 0x20);
+ inst32[0] = BSWAP32(inst32[0]); // sampleRate
+ // 0x24-0x2B are u8 fields, no swap needed
+
+ inst32 = reinterpret_cast<uint32_t*>(instData + 0x2C);
+ uint32_t envOffset = BSWAP32(inst32[0]);
+ inst32[0] = envOffset; // envelopes
+
+ // Swap EnvelopePreset data if not already done
+ // EnvelopePreset: u8 count, pad[3], EnvelopeOffset offsets[count]
+ // EnvelopeOffset: u16 offsetPress, u16 offsetRelease
+ if (envOffset != 0 && swappedEnvelopes.find(envOffset) == swappedEnvelopes.end()) {
+ uint32_t envAbsOff = fileOffset + envOffset;
+ if (CHECK_BOUNDS(envAbsOff, 4, size)) {
+ uint8_t envCount = data[envAbsOff]; // u8, no swap
+ // Swap each EnvelopeOffset entry (4 bytes each)
+ for (uint8_t e = 0; e < envCount; e++) {
+ uint32_t entryOff = envAbsOff + 4 + e * 4;
+ if (CHECK_BOUNDS(entryOff, 4, size)) {
+ uint16_t* envEntry = reinterpret_cast<uint16_t*>(data + entryOff);
+ envEntry[0] = BSWAP16(envEntry[0]); // offsetPress
+ envEntry[1] = BSWAP16(envEntry[1]); // offsetRelease
+ }
+ }
+ swappedEnvelopes.insert(envOffset);
+ }
+ }
+ }
+ }
+
+ // Swap predictor codebook data (s16 array)
+ if (predictorsStart > 0 && predictorsLength > 0) {
+ uint32_t predAbsOff = fileOffset + predictorsStart;
+ if (CHECK_BOUNDS(predAbsOff, predictorsLength, size)) {
+ uint16_t* predData = reinterpret_cast<uint16_t*>(data + predAbsOff);
+ uint32_t numShorts = predictorsLength / 2;
+ for (uint32_t p = 0; p < numShorts; p++) {
+ predData[p] = BSWAP16(predData[p]);
+ }
+ SPDLOG_DEBUG("BK: swapped {} predictor shorts at offset 0x{:X}", numShorts, predictorsStart);
+ }
+ }
+
+ // Swap loop state data (s16 array)
+ if (loopStatesStart > 0 && loopStatesLength > 0) {
+ uint32_t loopAbsOff = fileOffset + loopStatesStart;
+ if (CHECK_BOUNDS(loopAbsOff, loopStatesLength, size)) {
+ uint16_t* loopData = reinterpret_cast<uint16_t*>(data + loopAbsOff);
+ uint32_t numShorts = loopStatesLength / 2;
+ for (uint32_t l = 0; l < numShorts; l++) {
+ loopData[l] = BSWAP16(loopData[l]);
+ }
+ SPDLOG_DEBUG("BK: swapped {} loop state shorts at offset 0x{:X}", numShorts, loopStatesStart);
+ }
+ }
+ }
+ break;
+ }
+
+ case AU_FMT_SEF: {
+ // SEF Header: s32 signature, s32 size, s32 name, pad[2], u8 hasExtraSection, pad[1],
+ // u16 sections[8], u16 section2000
+ if (CHECK_BOUNDS(fileOffset, SEF_HEADER_SIZE, size)) {
+ uint32_t* sef32 = reinterpret_cast<uint32_t*>(fileData);
+ sef32[0] = BSWAP32(sef32[0]); // signature
+ uint32_t sefSize = BSWAP32(sef32[1]); // size
+ sef32[1] = sefSize;
+ sef32[2] = BSWAP32(sef32[2]); // name
+ // pad and u8 at 0x0C-0x0F
+
+ // Swap header section offsets and save them
+ uint16_t* sef16 = reinterpret_cast<uint16_t*>(fileData + 0x10);
+ uint16_t sectionOffsets[9]; // sections[8] + section2000
+ for (int j = 0; j < 9; j++) {
+ sectionOffsets[j] = BSWAP16(sef16[j]);
+ sef16[j] = sectionOffsets[j];
+ }
+
+ // SEF section layout:
+ // - Sections 0-3: lookup tables of (u16 offset, u16 info) pairs + polyphonic sub-tables
+ // Game code dereferences offset via AU_FILE_RELATIVE -> must be swapped
+ // - Sections 4-7 and extra: raw command bytes passed directly as (u8*)cmdList
+ // Game code reads byte-by-byte -> must NOT be swapped
+
+ // Track swapped sub-table offsets to avoid double-swapping
+ std::set<uint16_t> swappedSubTables;
+
+ // Swap sections 0-3 lookup tables
+ for (int j = 0; j < 4; j++) {
+ if (sectionOffsets[j] == 0) continue;
+ uint32_t secAbsOff = fileOffset + sectionOffsets[j];
+ uint32_t entryCount = SEF_SECTION_0_3_ENTRIES;
+ if (!CHECK_BOUNDS(secAbsOff, entryCount * 4, size)) continue;
+
+ uint16_t* entries = reinterpret_cast<uint16_t*>(data + secAbsOff);
+ for (uint32_t k = 0; k < entryCount; k++) {
+ uint16_t cmdOffset = BSWAP16(entries[k * 2]);
+ uint16_t cmdInfo = BSWAP16(entries[k * 2 + 1]);
+ entries[k * 2] = cmdOffset;
+ entries[k * 2 + 1] = cmdInfo;
+
+ if (cmdOffset == 0) continue;
+
+ // Check for polyphonic entries (bits 5-6 of info)
+ uint8_t polyphonyMode = (cmdInfo & 0x60) >> 5;
+ if (polyphonyMode != 0 && swappedSubTables.find(cmdOffset) == swappedSubTables.end()) {
+ // Follow offset to polyphonic sub-table and swap it
+ uint32_t trackCount = 2 << (polyphonyMode - 1); // 2, 4, or 8
+ uint32_t subTableAbsOff = fileOffset + cmdOffset;
+ if (CHECK_BOUNDS(subTableAbsOff, trackCount * 4, size)) {
+ uint16_t* subEntries = reinterpret_cast<uint16_t*>(data + subTableAbsOff);
+ for (uint32_t t = 0; t < trackCount; t++) {
+ subEntries[t * 2] = BSWAP16(subEntries[t * 2]); // offset
+ subEntries[t * 2 + 1] = BSWAP16(subEntries[t * 2 + 1]); // info
+ }
+ }
+ swappedSubTables.insert(cmdOffset);
+ }
+ }
+ }
+
+ // Sections 4-7: raw command bytes, no swap needed
+ // Extra section (section2000): raw command bytes, no swap needed
+
+ SPDLOG_DEBUG("SEF: swapped {} section 0-3 lookup tables, {} polyphonic sub-tables",
+ 4, swappedSubTables.size());
+ }
+ break;
+ }
+
+ case AU_FMT_MSEQ: {
+ // Format 0x40 is shared by MSEQ, PER, and PRG files.
+ // Distinguish by reading the big-endian signature before swapping.
+ uint32_t fileSig = BSWAP32(*reinterpret_cast<uint32_t*>(fileData));
+
+ if (fileSig == PER_SIGNATURE) {
+ // PER file: s32 signature, s32 size, pad[8], then PEREntry data
+ // PEREntry = 12 × BGMDrumInfo (0x0C bytes each) = 0x90 bytes
+ // BGMDrumInfo: u16 bankPatch, u16 keyBase, u8 volume, s8 pan, u8 reverb, ...
+ if (CHECK_BOUNDS(fileOffset, PER_HEADER_SIZE, size)) {
+ uint32_t* per32 = reinterpret_cast<uint32_t*>(fileData);
+ per32[0] = BSWAP32(per32[0]); // signature
+ uint32_t perSize = BSWAP32(per32[1]);
+ per32[1] = perSize; // size
+
+ // Swap internal BGMDrumInfo entries
+ uint32_t dataStart = PER_HEADER_SIZE;
+ uint32_t dataLen = perSize - dataStart;
+ uint32_t numDrums = dataLen / BGM_DRUM_INFO_SIZE;
+ for (uint32_t d = 0; d < numDrums; d++) {
+ uint32_t drumOff = fileOffset + dataStart + d * BGM_DRUM_INFO_SIZE;
+ if (CHECK_BOUNDS(drumOff, BGM_DRUM_INFO_SIZE, size)) {
+ uint16_t* drum16 = reinterpret_cast<uint16_t*>(data + drumOff);
+ drum16[0] = BSWAP16(drum16[0]); // bankPatch
+ drum16[1] = BSWAP16(drum16[1]); // keyBase
+ // remaining fields are u8, no swap
+ }
+ }
+ SPDLOG_DEBUG("PER: swapped {} drum entries", numDrums);
+ }
+ } else if (fileSig == PRG_SIGNATURE) {
+ // PRG file: s32 signature, s32 size, pad[8], then BGMInstrumentInfo data
+ // BGMInstrumentInfo: u16 bankPatch, u8 volume, s8 pan, u8 reverb, s8 coarseTune, s8 fineTune, pad
+ if (CHECK_BOUNDS(fileOffset, PER_HEADER_SIZE, size)) {
+ uint32_t* prg32 = reinterpret_cast<uint32_t*>(fileData);
+ prg32[0] = BSWAP32(prg32[0]); // signature
+ uint32_t prgSize = BSWAP32(prg32[1]);
+ prg32[1] = prgSize; // size
+
+ // Swap internal BGMInstrumentInfo entries
+ uint32_t dataStart = PER_HEADER_SIZE;
+ uint32_t dataLen = prgSize - dataStart;
+ uint32_t numInst = dataLen / BGM_INSTRUMENT_INFO_SIZE;
+ for (uint32_t p = 0; p < numInst; p++) {
+ uint32_t instOff = fileOffset + dataStart + p * BGM_INSTRUMENT_INFO_SIZE;
+ if (CHECK_BOUNDS(instOff, BGM_INSTRUMENT_INFO_SIZE, size)) {
+ uint16_t* inst16 = reinterpret_cast<uint16_t*>(data + instOff);
+ inst16[0] = BSWAP16(inst16[0]); // bankPatch
+ // remaining fields are u8/s8, no swap
+ }
+ }
+ SPDLOG_DEBUG("PRG: swapped {} instrument entries", numInst);
+ }
+ } else {
+ // MSEQ file
+ // MSEQ Header: s32 signature, s32 size, s32 name, u8 firstVoiceIdx, u8 trackSettingsCount,
+ // u16 trackSettingsOffset, u16 dataStart, pad[6]
+ if (CHECK_BOUNDS(fileOffset, MSEQ_HEADER_SIZE, size)) {
+ uint32_t* mseq32 = reinterpret_cast<uint32_t*>(fileData);
+ mseq32[0] = BSWAP32(mseq32[0]); // signature
+ mseq32[1] = BSWAP32(mseq32[1]); // size
+ mseq32[2] = BSWAP32(mseq32[2]); // name
+ // u8 fields at 0x0C-0x0D
+
+ uint16_t* mseq16 = reinterpret_cast<uint16_t*>(fileData + 0x0E);
+ uint16_t trackSettingsOffset = BSWAP16(mseq16[0]);
+ mseq16[0] = trackSettingsOffset; // trackSettingsOffset
+ mseq16[1] = BSWAP16(mseq16[1]); // dataStart
+
+ // Swap MSEQTrackData entries
+ // Each entry: u8 trackIndex, u8 type, s16 time, s16 delta, s16 goal (8 bytes)
+ uint8_t trackSettingsCount = fileData[0x0D];
+ if (trackSettingsCount > 0 && trackSettingsOffset > 0) {
+ for (uint8_t t = 0; t < trackSettingsCount; t++) {
+ uint32_t entryOff = fileOffset + trackSettingsOffset + t * 8;
+ if (CHECK_BOUNDS(entryOff, 8, size)) {
+ uint16_t* td16 = reinterpret_cast<uint16_t*>(data + entryOff + 2);
+ td16[0] = BSWAP16(td16[0]); // time
+ td16[1] = BSWAP16(td16[1]); // delta
+ td16[2] = BSWAP16(td16[2]); // goal
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ default:
+ // Unknown file type, skip
+ break;
+ }
+ }
+ }
+ }
+
+ // === INIT Section ===
+ if (initOffset > 0 && CHECK_BOUNDS(initOffset, INIT_HEADER_SIZE, size)) {
+ uint8_t* initData = data + initOffset;
+
+ // INIT Header: s32 signature, s32 size, u16 bankListOffset, u16 bankListSize,
+ // u16 songListOffset, u16 songListSize, u16 mseqListOffset, u16 mseqListSize, pad[12]
+ uint32_t* init32 = reinterpret_cast<uint32_t*>(initData);
+ init32[0] = BSWAP32(init32[0]); // signature
+ init32[1] = BSWAP32(init32[1]); // size
+
+ uint16_t* init16 = reinterpret_cast<uint16_t*>(initData + 0x08);
+ uint16_t bankListOffset = BSWAP16(init16[0]);
+ uint16_t bankListSize = BSWAP16(init16[1]);
+ uint16_t songListOffset = BSWAP16(init16[2]);
+ uint16_t songListSize = BSWAP16(init16[3]);
+ uint16_t mseqListOffset = BSWAP16(init16[4]);
+ uint16_t mseqListSize = BSWAP16(init16[5]);
+
+ init16[0] = bankListOffset;
+ init16[1] = bankListSize;
+ init16[2] = songListOffset;
+ init16[3] = songListSize;
+ init16[4] = mseqListOffset;
+ init16[5] = mseqListSize;
+
+ SPDLOG_DEBUG("INIT: songListOffset=0x{:X}, songListSize={}, bankListOffset=0x{:X}, mseqListOffset=0x{:X}",
+ songListOffset, songListSize, bankListOffset, mseqListOffset);
+
+ // === Song List (InitSongEntry array) ===
+ // Each entry: u16 bgmFileIndex, u16 bkFileIndex[3]
+ uint32_t songListAbsOffset = initOffset + songListOffset;
+ uint32_t numSongs = songListSize / INIT_SONG_ENTRY_SIZE;
+ if (songListOffset > 0 && CHECK_BOUNDS(songListAbsOffset, songListSize, size)) {
+ uint16_t* songList = reinterpret_cast<uint16_t*>(data + songListAbsOffset);
+ for (uint32_t i = 0; i < numSongs * 4; i++) { // 4 u16s per entry
+ songList[i] = BSWAP16(songList[i]);
+ }
+ }
+
+ // === Bank List (InitBankEntry array) ===
+ // Each entry: u16 fileIndex, u8 bankIndex, u8 bankSet
+ uint32_t bankListAbsOffset = initOffset + bankListOffset;
+ uint32_t numBanks = bankListSize / INIT_BANK_ENTRY_SIZE;
+ if (bankListOffset > 0 && CHECK_BOUNDS(bankListAbsOffset, bankListSize, size)) {
+ // Only need to swap the u16 fileIndex, the u8 fields don't need swapping
+ for (uint32_t i = 0; i < numBanks; i++) {
+ uint16_t* bankEntry = reinterpret_cast<uint16_t*>(data + bankListAbsOffset + i * INIT_BANK_ENTRY_SIZE);
+ bankEntry[0] = BSWAP16(bankEntry[0]); // fileIndex
+ }
+ }
+
+ // === MSEQ/Extra File List (u16 array) ===
+ // This is a simple array of u16 file indices
+ uint32_t mseqListAbsOffset = initOffset + mseqListOffset;
+ uint32_t numMseqEntries = mseqListSize / 2;
+ if (mseqListOffset > 0 && CHECK_BOUNDS(mseqListAbsOffset, mseqListSize, size)) {
+ uint16_t* mseqList = reinterpret_cast<uint16_t*>(data + mseqListAbsOffset);
+ for (uint32_t i = 0; i < numMseqEntries; i++) {
+ mseqList[i] = BSWAP16(mseqList[i]);
+ }
+ }
+ }
+
+ SPDLOG_DEBUG("PM64:AUDIO byte-swap complete, {} bytes processed", size);
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64AudioFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+ auto size = GetSafeNode<size_t>(node, "size");
+
+ if (offset + size > buffer.size()) {
+ SPDLOG_ERROR("PM64:AUDIO offset 0x{:X} + size 0x{:X} exceeds buffer size 0x{:X}", offset, size, buffer.size());
+ return std::nullopt;
+ }
+
+ // Copy the audio data
+ std::vector<uint8_t> audioData(buffer.begin() + offset, buffer.begin() + offset + size);
+
+ // Byte-swap for little-endian
+ ByteSwapAudioData(audioData.data(), audioData.size());
+
+ return std::make_shared<RawBuffer>(audioData);
+}
+
+ExportResult PM64AudioBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ // Write as Blob type - game will load as raw binary
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64AudioHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/AudioFactory.h b/src/factories/pm64/AudioFactory.h
new file mode 100644
index 0000000..13b202f
--- /dev/null
+++ b/src/factories/pm64/AudioFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64AudioBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64AudioHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64AudioFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64AudioHeaderExporter)
+ REGISTER(Binary, PM64AudioBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/BackgroundFactory.cpp b/src/factories/pm64/BackgroundFactory.cpp
new file mode 100644
index 0000000..a745cd5
--- /dev/null
+++ b/src/factories/pm64/BackgroundFactory.cpp
@@ -0,0 +1,104 @@
+#include "BackgroundFactory.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include "spdlog/spdlog.h"
+
+// PM64 background file structure (N64 ROM format):
+// BackgroundHeader (0x10 bytes):
+// 0x00: rasterAddr (u32) - N64 VRAM address (e.g., 0x80200210) - NOT a file offset!
+// 0x04: paletteAddr (u32) - N64 VRAM address (e.g., 0x80200010) - NOT a file offset!
+// 0x08: startX (u16)
+// 0x0A: startY (u16)
+// 0x0C: width (u16)
+// 0x0E: height (u16)
+//
+// The decompressed data has a FIXED layout:
+// 0x0000: Header (16 bytes)
+// 0x0010: Palette (256 colors * 2 bytes = 512 bytes, RGBA16)
+// 0x0210: Raster (width * height bytes, CI8 indexed)
+//
+// The N64 VRAM addresses in the header are meaningless on PC - we use fixed offsets.
+
+static void ByteSwapBackgroundData(uint8_t* data, size_t size) {
+ if (size < 0x10) {
+ SPDLOG_WARN("Background data too small: {}", size);
+ return;
+ }
+
+ uint32_t* header32 = reinterpret_cast<uint32_t*>(data);
+ uint16_t* header16 = reinterpret_cast<uint16_t*>(data);
+
+ // Swap 16-bit dimension fields first (we need these for validation)
+ header16[4] = BSWAP16(header16[4]); // startX at offset 0x08
+ header16[5] = BSWAP16(header16[5]); // startY at offset 0x0A
+ header16[6] = BSWAP16(header16[6]); // width at offset 0x0C
+ header16[7] = BSWAP16(header16[7]); // height at offset 0x0E
+
+ // The N64 header contains absolute VRAM addresses (0x802xxxxx) that cannot be
+ // converted to file offsets. The background data has a fixed layout:
+ // - Palette at offset 0x10 (right after 16-byte header)
+ // - Raster at offset 0x210 (after header + 512-byte palette)
+ // We ignore the N64 addresses and write the correct fixed offsets.
+ constexpr uint32_t paletteOffset = 0x10; // Right after 16-byte header
+ constexpr uint32_t rasterOffset = 0x210; // After header (16) + palette (512)
+
+ header32[0] = rasterOffset;
+ header32[1] = paletteOffset;
+
+ // Background palettes are swapped internally by libultraship, no need to swap them here
+ // Also, Raster data is CI8 (byte indices) - no swap needed
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64BackgroundFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+
+ // Check if compressed (YAY0)
+ auto compressionType = Decompressor::GetCompressionType(buffer, offset);
+
+ if (compressionType == CompressionType::YAY0) {
+ auto decoded = Decompressor::Decode(buffer, offset, CompressionType::YAY0);
+ if (!decoded || decoded->size == 0) {
+ SPDLOG_ERROR("Failed to decompress YAY0 background data at offset 0x{:X}", offset);
+ return std::nullopt;
+ }
+
+ std::vector<uint8_t> bgData(decoded->data, decoded->data + decoded->size);
+ ByteSwapBackgroundData(bgData.data(), bgData.size());
+
+ return std::make_shared<RawBuffer>(bgData);
+ } else {
+ // Uncompressed - read raw data with size from YAML
+ auto size = GetSafeNode<size_t>(node, "size");
+ auto [_, segment] = Decompressor::AutoDecode(node, buffer, size);
+
+ std::vector<uint8_t> bgData(segment.data, segment.data + segment.size);
+ ByteSwapBackgroundData(bgData.data(), bgData.size());
+
+ return std::make_shared<RawBuffer>(bgData);
+ }
+}
+
+ExportResult PM64BackgroundBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ // Write as Blob type - game loads as raw binary
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64BackgroundHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/BackgroundFactory.h b/src/factories/pm64/BackgroundFactory.h
new file mode 100644
index 0000000..099bafb
--- /dev/null
+++ b/src/factories/pm64/BackgroundFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64BackgroundBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64BackgroundHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64BackgroundFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64BackgroundHeaderExporter)
+ REGISTER(Binary, PM64BackgroundBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/CollisionFactory.cpp b/src/factories/pm64/CollisionFactory.cpp
new file mode 100644
index 0000000..55e6a98
--- /dev/null
+++ b/src/factories/pm64/CollisionFactory.cpp
@@ -0,0 +1,192 @@
+#include "CollisionFactory.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include "spdlog/spdlog.h"
+
+// PM64 collision file structure (N64 ROM format):
+//
+// HitFile header (8 bytes):
+// 0x00: collisionOffset (u32) - offset to collision HitFileHeader
+// 0x04: zoneOffset (u32) - offset to zone HitFileHeader
+//
+// HitFileHeader (0x18 bytes) at collisionOffset and zoneOffset:
+// 0x00: numColliders (s16)
+// 0x02: pad (2 bytes)
+// 0x04: collidersOffset (s32) - offset to HitAssetCollider array
+// 0x08: numVertices (s16)
+// 0x0A: pad (2 bytes)
+// 0x0C: verticesOffset (s32) - offset to Vec3s array
+// 0x10: boundingBoxesDataSize (s16)
+// 0x12: pad (2 bytes)
+// 0x14: boundingBoxesOffset (s32) - offset to bounding box data
+//
+// HitAssetCollider (0x0C bytes) - array of numColliders entries:
+// 0x00: boundingBoxOffset (s16)
+// 0x02: nextSibling (s16)
+// 0x04: firstChild (s16)
+// 0x06: numTriangles (s16)
+// 0x08: trianglesOffset (s32) - offset to packed triangle data
+//
+// Vertices: Vec3s array (6 bytes each) - numVertices entries
+// Each x, y, z is s16
+//
+// Triangles: s32 array - packed vertex indices + flags
+// bits 0-9: v1, 10-19: v2, 20-29: v3, 30: oneSided
+
+static void ByteSwapHitFileHeader(uint8_t* data, uint32_t headerOffset, size_t totalSize) {
+ if (headerOffset == 0 || headerOffset + 0x18 > totalSize) {
+ return;
+ }
+
+ uint8_t* header = data + headerOffset;
+
+ // numColliders at 0x00 (s16)
+ uint16_t* numColliders = reinterpret_cast<uint16_t*>(header);
+ *numColliders = BSWAP16(*numColliders);
+
+ // collidersOffset at 0x04 (s32)
+ uint32_t* collidersOffset = reinterpret_cast<uint32_t*>(header + 0x04);
+ *collidersOffset = BSWAP32(*collidersOffset);
+
+ // numVertices at 0x08 (s16)
+ uint16_t* numVertices = reinterpret_cast<uint16_t*>(header + 0x08);
+ *numVertices = BSWAP16(*numVertices);
+
+ // verticesOffset at 0x0C (s32)
+ uint32_t* verticesOffset = reinterpret_cast<uint32_t*>(header + 0x0C);
+ *verticesOffset = BSWAP32(*verticesOffset);
+
+ // boundingBoxesDataSize at 0x10 (s16)
+ uint16_t* boundingBoxesDataSize = reinterpret_cast<uint16_t*>(header + 0x10);
+ *boundingBoxesDataSize = BSWAP16(*boundingBoxesDataSize);
+
+ // boundingBoxesOffset at 0x14 (s32)
+ uint32_t* boundingBoxesOffset = reinterpret_cast<uint32_t*>(header + 0x14);
+ *boundingBoxesOffset = BSWAP32(*boundingBoxesOffset);
+
+ SPDLOG_DEBUG("HitFileHeader at 0x{:X}: numColliders={}, collidersOffset=0x{:X}, numVertices={}, verticesOffset=0x{:X}, bbSize={}, bbOffset=0x{:X}",
+ headerOffset, *numColliders, *collidersOffset, *numVertices, *verticesOffset,
+ *boundingBoxesDataSize, *boundingBoxesOffset);
+
+ // Byte-swap colliders array (HitAssetCollider, 0x0C bytes each)
+ uint32_t collOffset = *collidersOffset;
+ uint16_t numColl = *numColliders;
+ if (collOffset > 0 && collOffset + numColl * 0x0C <= totalSize) {
+ for (uint16_t i = 0; i < numColl; i++) {
+ uint8_t* collider = data + collOffset + i * 0x0C;
+ uint16_t* s16Fields = reinterpret_cast<uint16_t*>(collider);
+ s16Fields[0] = BSWAP16(s16Fields[0]); // boundingBoxOffset
+ s16Fields[1] = BSWAP16(s16Fields[1]); // nextSibling
+ s16Fields[2] = BSWAP16(s16Fields[2]); // firstChild
+ s16Fields[3] = BSWAP16(s16Fields[3]); // numTriangles
+ uint32_t* trianglesOffset = reinterpret_cast<uint32_t*>(collider + 0x08);
+ *trianglesOffset = BSWAP32(*trianglesOffset);
+
+ // Byte-swap triangles array for this collider (s32 each)
+ uint16_t numTris = s16Fields[3];
+ uint32_t trisOffset = *trianglesOffset;
+ if (numTris > 0 && trisOffset > 0 && trisOffset + numTris * 4 <= totalSize) {
+ uint32_t* triangles = reinterpret_cast<uint32_t*>(data + trisOffset);
+ for (uint16_t t = 0; t < numTris; t++) {
+ triangles[t] = BSWAP32(triangles[t]);
+ }
+ }
+ }
+ }
+
+ // Byte-swap vertices array (Vec3s, 6 bytes each - 3 x s16)
+ uint32_t vertOffset = *verticesOffset;
+ uint16_t numVerts = *numVertices;
+ if (vertOffset > 0 && vertOffset + numVerts * 6 <= totalSize) {
+ uint16_t* vertices = reinterpret_cast<uint16_t*>(data + vertOffset);
+ for (uint16_t i = 0; i < numVerts * 3; i++) {
+ vertices[i] = BSWAP16(vertices[i]);
+ }
+ }
+
+ // Byte-swap bounding boxes data (u32 array)
+ uint32_t bbOffset = *boundingBoxesOffset;
+ uint16_t bbSize = *boundingBoxesDataSize;
+ if (bbOffset > 0 && bbSize > 0 && bbOffset + bbSize * 4 <= totalSize) {
+ uint32_t* bboxes = reinterpret_cast<uint32_t*>(data + bbOffset);
+ for (uint16_t i = 0; i < bbSize; i++) {
+ bboxes[i] = BSWAP32(bboxes[i]);
+ }
+ }
+}
+
+static void ByteSwapCollisionData(uint8_t* data, size_t size) {
+ if (size < 8) {
+ SPDLOG_WARN("Collision data too small: {}", size);
+ return;
+ }
+
+ // Byte-swap HitFile header
+ uint32_t* header = reinterpret_cast<uint32_t*>(data);
+ uint32_t collisionOffset = BSWAP32(header[0]);
+ uint32_t zoneOffset = BSWAP32(header[1]);
+ header[0] = collisionOffset;
+ header[1] = zoneOffset;
+
+ SPDLOG_DEBUG("HitFile: collisionOffset=0x{:X}, zoneOffset=0x{:X}", collisionOffset, zoneOffset);
+
+ // Byte-swap collision HitFileHeader and its data
+ ByteSwapHitFileHeader(data, collisionOffset, size);
+
+ // Byte-swap zone HitFileHeader and its data
+ ByteSwapHitFileHeader(data, zoneOffset, size);
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64CollisionFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+
+ // Check if compressed (YAY0)
+ auto compressionType = Decompressor::GetCompressionType(buffer, offset);
+
+ if (compressionType == CompressionType::YAY0) {
+ auto decoded = Decompressor::Decode(buffer, offset, CompressionType::YAY0);
+ if (!decoded || decoded->size == 0) {
+ SPDLOG_ERROR("Failed to decompress YAY0 collision data at offset 0x{:X}", offset);
+ return std::nullopt;
+ }
+
+ std::vector<uint8_t> colData(decoded->data, decoded->data + decoded->size);
+ ByteSwapCollisionData(colData.data(), colData.size());
+
+ return std::make_shared<RawBuffer>(colData);
+ } else {
+ // Uncompressed - read raw data with size from YAML
+ auto size = GetSafeNode<size_t>(node, "size");
+ auto [_, segment] = Decompressor::AutoDecode(node, buffer, size);
+
+ std::vector<uint8_t> colData(segment.data, segment.data + segment.size);
+ ByteSwapCollisionData(colData.data(), colData.size());
+
+ return std::make_shared<RawBuffer>(colData);
+ }
+}
+
+ExportResult PM64CollisionBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ // Write as Blob type - game loads as raw binary
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64CollisionHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/CollisionFactory.h b/src/factories/pm64/CollisionFactory.h
new file mode 100644
index 0000000..617cbcc
--- /dev/null
+++ b/src/factories/pm64/CollisionFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64CollisionBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64CollisionHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64CollisionFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64CollisionHeaderExporter)
+ REGISTER(Binary, PM64CollisionBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/EffectDListFactory.cpp b/src/factories/pm64/EffectDListFactory.cpp
new file mode 100644
index 0000000..b179c4c
--- /dev/null
+++ b/src/factories/pm64/EffectDListFactory.cpp
@@ -0,0 +1,235 @@
+// PM64 Effect Display List Factory
+//
+// Overrides the standard DListFactory binary exporter to fix a width encoding bug
+// in torch's G_SETTIMG → G_SETTIMG_OTR_HASH conversion.
+//
+// The standard DListFactory uses gsDPSetTextureOTRImage(fmt, siz, C0(0,10), ptr)
+// which double-subtracts 1 from the width (the original DL already stores width-1,
+// and the macro subtracts 1 again). This causes texture_to_load.width to be off by 1,
+// which breaks effects that use G_LOADTILE (e.g. sparkles with its 176x22 texture strip).
+//
+// This factory preserves the original w0 bits exactly:
+// newW0 = (G_SETTIMG_OTR_HASH << 24) | (w0 & 0x00FFFFFF)
+
+#include "EffectDListFactory.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include "spdlog/spdlog.h"
+#include "n64/gbi-otr.h"
+#include "strhash64/StrHash64.h"
+
+// F3DEX2 opcodes
+#define F3DEX2_G_VTX 0x01
+#define F3DEX2_G_DL 0xDE
+#define F3DEX2_G_MTX 0xDA
+#define F3DEX2_G_ENDDL 0xDF
+#define F3DEX2_G_SETTIMG 0xFD
+#define F3DEX2_G_MOVEMEM 0xDC
+
+#define C0(pos, width) ((w0 >> (pos)) & ((1U << width) - 1))
+
+// Override DListFactory::parse to skip auto-discovery of sub-DL/VTX/light assets.
+// All effect assets are already defined in the YAML files, so auto-discovery via
+// Companion::Instance->AddAsset() is unnecessary and harmful: it re-registers
+// sub-DLs as "GFX" type (standard DListFactory), overwriting the YAML-defined
+// "PM64:EFFECT_DL" entries and causing them to be exported by the standard
+// DListBinaryExporter instead of PM64EffectDListBinaryExporter.
+std::optional<std::shared_ptr<IParsedData>> PM64EffectDListFactory::parse(std::vector<uint8_t>& raw_buffer, YAML::Node& node) {
+ auto [_, segment] = Decompressor::AutoDecode(node, raw_buffer);
+ LUS::BinaryReader reader(segment.data, segment.size);
+ reader.SetEndianness(Torch::Endianness::Big);
+
+ std::vector<uint32_t> gfxs;
+ bool processing = true;
+
+ while (processing) {
+ auto w0 = reader.ReadUInt32();
+ auto w1 = reader.ReadUInt32();
+ uint8_t opcode = w0 >> 24;
+
+ if (opcode == F3DEX2_G_ENDDL) {
+ processing = false;
+ }
+
+ if (opcode == F3DEX2_G_DL) {
+ // If this is a branch (G_DL_NO_PUSH), stop processing like the base class does
+ if ((w0 >> 16) & 0x01) {
+ processing = false;
+ }
+ // Intentionally skip Companion::Instance->AddAsset() — assets defined in YAML
+ }
+
+ gfxs.push_back(w0);
+ gfxs.push_back(w1);
+ }
+
+ return std::make_shared<DListData>(gfxs);
+}
+
+ExportResult PM64EffectDListBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto cmds = std::static_pointer_cast<DListData>(raw)->mGfxs;
+ auto writer = LUS::BinaryWriter();
+
+ WriteHeader(writer, Torch::ResourceType::DisplayList, 0);
+
+ // GBI version byte (F3DEX2)
+ writer.Write(static_cast<int8_t>(GBIVersion::f3dex2));
+
+ // Pad to 8-byte alignment
+ while (writer.GetBaseAddress() % 8 != 0)
+ writer.Write(static_cast<int8_t>(0xFF));
+
+ // G_MARKER with resource hash
+ auto bhash = CRC64((*replacement).c_str());
+ writer.Write(static_cast<uint32_t>(G_MARKER << 24));
+ writer.Write(static_cast<uint32_t>(0xBEEFBEEF));
+ writer.Write(static_cast<uint32_t>(bhash >> 32));
+ writer.Write(static_cast<uint32_t>(bhash & 0xFFFFFFFF));
+
+ for (size_t i = 0; i < cmds.size(); i += 2) {
+ auto w0 = cmds[i];
+ auto w1 = cmds[i + 1];
+ uint8_t opcode = w0 >> 24;
+
+ if (opcode == F3DEX2_G_VTX) {
+ auto ptr = w1;
+ auto dec = Companion::Instance->GetSafeStringByAddr(ptr, "VTX");
+
+ if (dec.has_value()) {
+ uint64_t hash = CRC64(dec.value().c_str());
+ if (hash == 0) {
+ throw std::runtime_error("Vtx hash is 0 for " + dec.value());
+ }
+ // Construct G_VTX_OTR_HASH: preserve n/v0 bits, zero the vtx offset
+ uint32_t newW0 = (G_VTX_OTR_HASH << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(hash & 0xFFFFFFFF));
+ } else {
+ SPDLOG_WARN("Could not find vtx at 0x{:X}", ptr);
+ writer.Write(w0);
+ writer.Write(w1);
+ }
+ continue;
+ }
+
+ if (opcode == F3DEX2_G_DL) {
+ auto ptr = w1;
+ // Use GetStringByAddr (no type check) since sub-DLs are registered as
+ // "PM64:EFFECT_DL" in YAML, not "GFX" as the standard factory expects.
+ auto dec = Companion::Instance->GetStringByAddr(ptr);
+ auto branch = (w0 >> 16) & 0x01; // G_DL_NO_PUSH
+
+ // Construct G_DL_OTR_HASH
+ uint32_t newW0 = (G_DL_OTR_HASH << 24) | (branch << 16);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+
+ if (dec.has_value()) {
+ uint64_t hash = CRC64(dec.value().c_str());
+ writer.Write(static_cast<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(hash & 0xFFFFFFFF));
+ } else {
+ SPDLOG_WARN("Could not find display list at 0x{:X}", ptr);
+ writer.Write(w0);
+ writer.Write(w1);
+ }
+
+ if (branch) {
+ // Append G_ENDDL after a branch
+ writer.Write(static_cast<uint32_t>(F3DEX2_G_ENDDL << 24));
+ writer.Write(static_cast<uint32_t>(0));
+ }
+ continue;
+ }
+
+ if (opcode == F3DEX2_G_MOVEMEM) {
+ auto ptr = w1;
+ auto res = Companion::Instance->GetStringByAddr(ptr);
+ bool hasOffset = false;
+
+ if (!res.has_value()) {
+ res = Companion::Instance->GetStringByAddr(ptr - 0x8);
+ hasOffset = res.has_value();
+ if (!hasOffset) {
+ SPDLOG_WARN("Could not find light {:X}", ptr);
+ }
+ }
+
+ uint8_t index = C0(0, 8);
+ uint8_t offset = C0(8, 8) * 8;
+
+ uint32_t newW0 = (G_MOVEMEM_OTR_HASH << 24) | (w0 & 0x00FFFFFF);
+ uint32_t newW1 = _SHIFTL(index, 24, 8) | _SHIFTL(offset, 16, 8) | _SHIFTL((uint8_t)(hasOffset ? 1 : 0), 8, 8);
+
+ writer.Write(newW0);
+ writer.Write(newW1);
+
+ if (res.has_value()) {
+ uint64_t hash = CRC64(res.value().c_str());
+ writer.Write(static_cast<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(hash & 0xFFFFFFFF));
+ } else {
+ SPDLOG_WARN("Could not find light at 0x{:X}", ptr);
+ writer.Write(w0);
+ writer.Write(w1);
+ }
+ continue;
+ }
+
+ if (opcode == F3DEX2_G_SETTIMG) {
+ auto ptr = w1;
+ auto dec = Companion::Instance->GetSafeStringByAddr(ptr, "TEXTURE");
+
+ // FIX: Preserve original w0 bits (fmt/siz/width) exactly.
+ uint32_t newW0 = (G_SETTIMG_OTR_HASH << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(ptr);
+
+ if (dec.has_value()) {
+ uint64_t hash = CRC64(dec.value().c_str());
+ if (hash == 0) {
+ throw std::runtime_error("Texture hash is 0 for " + dec.value());
+ }
+ writer.Write(static_cast<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(hash & 0xFFFFFFFF));
+ } else {
+ SPDLOG_WARN("Could not find texture at 0x{:X}", ptr);
+ writer.Write(w0);
+ writer.Write(w1);
+ }
+ continue;
+ }
+
+ if (opcode == F3DEX2_G_MTX) {
+ auto ptr = w1;
+ auto dec = Companion::Instance->GetSafeStringByAddr(ptr, "MTX");
+
+ uint32_t newW0 = (G_MTX_OTR << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+
+ if (dec.has_value()) {
+ uint64_t hash = CRC64(dec.value().c_str());
+ if (hash == 0) {
+ throw std::runtime_error("Matrix hash is 0 for " + dec.value());
+ }
+ writer.Write(static_cast<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(hash & 0xFFFFFFFF));
+ } else {
+ SPDLOG_WARN("Could not find matrix at 0x{:X}", ptr);
+ writer.Write(w0);
+ writer.Write(w1);
+ }
+ continue;
+ }
+
+ // All other opcodes: pass through unchanged
+ writer.Write(w0);
+ writer.Write(w1);
+ }
+
+ writer.Finish(write);
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/EffectDListFactory.h b/src/factories/pm64/EffectDListFactory.h
new file mode 100644
index 0000000..9f9380a
--- /dev/null
+++ b/src/factories/pm64/EffectDListFactory.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "factories/DisplayListFactory.h"
+
+class PM64EffectDListBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64EffectDListFactory : public DListFactory {
+public:
+ // Override parse to skip auto-discovery of sub-DL/VTX/light assets.
+ // DListFactory::parse auto-discovers G_DL references and registers them as "GFX" type,
+ // which overwrites YAML-defined "PM64:EFFECT_DL" entries and causes sub-DLs to be
+ // exported by the standard DListBinaryExporter (with the width-subtraction bug).
+ // All effect assets are already defined in the YAML, so no auto-discovery is needed.
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, DListHeaderExporter)
+ REGISTER(Binary, PM64EffectDListBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/EntityGfxFactory.cpp b/src/factories/pm64/EntityGfxFactory.cpp
new file mode 100644
index 0000000..23f90b6
--- /dev/null
+++ b/src/factories/pm64/EntityGfxFactory.cpp
@@ -0,0 +1,569 @@
+#include "EntityGfxFactory.h"
+#include "Companion.h"
+#include "spdlog/spdlog.h"
+#include <unordered_set>
+#include <unordered_map>
+#include <sstream>
+#include "n64/gbi-otr.h"
+#include "strhash64/StrHash64.h"
+
+// F3DEX2 GBI opcodes
+#define F3DEX2_G_ENDDL 0xDF
+#define F3DEX2_G_VTX 0x01
+#define F3DEX2_G_DL 0xDE
+#define F3DEX2_G_SETTIMG 0xFD
+#define F3DEX2_G_MOVEMEM 0xDC
+#define F3DEX2_G_MTX 0xDA
+
+// Walk a display list at the given offset, byte-swap commands from BE to native,
+// collect them, and recursively process nested display lists.
+// Buffer is const — overlapping display lists share tail commands,
+// so we must never modify the shared ROM data.
+static void WalkDisplayList(const uint8_t* data, uint32_t offset, size_t bufferSize,
+ std::unordered_set<uint32_t>& visited,
+ std::vector<PM64EntityDisplayListInfo>& collected) {
+ if (offset >= bufferSize - 8) return;
+ if (visited.count(offset)) return;
+ visited.insert(offset);
+
+ const uint8_t* ptr = data + offset;
+ const uint8_t* endPtr = data + bufferSize;
+
+ PM64EntityDisplayListInfo dlInfo;
+ dlInfo.offset = offset;
+
+ while (ptr + 8 <= endPtr) {
+ const uint32_t* words = reinterpret_cast<const uint32_t*>(ptr);
+ uint32_t w0 = BSWAP32(words[0]);
+ uint32_t w1 = BSWAP32(words[1]);
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ // G_VTX: w1 is a segment 0xA address, strip segment byte
+ if (opcode == F3DEX2_G_VTX) {
+ w1 = w1 & 0x00FFFFFF;
+ }
+
+ // G_SETTIMG: w1 is a segment 0xA address, strip segment byte
+ if (opcode == F3DEX2_G_SETTIMG) {
+ w1 = w1 & 0x00FFFFFF;
+ }
+
+ // G_MOVEMEM: w1 is a segment 0xA address
+ if (opcode == F3DEX2_G_MOVEMEM) {
+ w1 = w1 & 0x00FFFFFF;
+ }
+
+ // G_MTX: w1 is a segment 0xA address
+ if (opcode == F3DEX2_G_MTX) {
+ w1 = w1 & 0x00FFFFFF;
+ }
+
+ // G_DL: w1 is a segment 0xA address, strip and recurse
+ if (opcode == F3DEX2_G_DL) {
+ uint32_t nestedOffset = w1 & 0x00FFFFFF;
+ w1 = nestedOffset;
+ WalkDisplayList(data, nestedOffset, bufferSize, visited, collected);
+ }
+
+ dlInfo.commands.push_back(w0);
+ dlInfo.commands.push_back(w1);
+
+ if (opcode == F3DEX2_G_ENDDL) {
+ break;
+ }
+ ptr += 8;
+ }
+
+ if (!dlInfo.commands.empty()) {
+ collected.push_back(std::move(dlInfo));
+ }
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64EntityGfxFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+ auto size = GetSafeNode<uint32_t>(node, "size");
+ auto dlistsNode = node["dlists"];
+
+ if (offset + size > buffer.size()) {
+ SPDLOG_ERROR("PM64:ENTITY_GFX: Data at offset 0x{:X} exceeds buffer (need {}, have {})",
+ offset, size, buffer.size() - offset);
+ return std::nullopt;
+ }
+
+ // Copy entity graphics data from ROM (initial size from YAML)
+ std::vector<uint8_t> entityData(buffer.data() + offset, buffer.data() + offset + size);
+
+ // Walk each display list specified in the YAML
+ std::unordered_set<uint32_t> visited;
+ std::vector<PM64EntityDisplayListInfo> collectedDLs;
+
+ if (dlistsNode && dlistsNode.IsSequence()) {
+ for (size_t i = 0; i < dlistsNode.size(); i++) {
+ uint32_t dlOffset = dlistsNode[i].as<uint32_t>();
+ if (dlOffset < size) {
+ WalkDisplayList(entityData.data(), dlOffset, entityData.size(), visited, collectedDLs);
+ }
+ }
+ }
+
+ // Find maximum offset referenced by any DL command (textures, vertices, matrices
+ // may live beyond the declared size in shared ROM space)
+ uint32_t maxReferencedEnd = size;
+ for (const auto& dl : collectedDLs) {
+ for (size_t i = 0; i < dl.commands.size(); i += 2) {
+ uint32_t w0 = dl.commands[i];
+ uint32_t w1 = dl.commands[i + 1];
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ if (opcode == F3DEX2_G_VTX) {
+ uint32_t n = (w0 >> 12) & 0xFF;
+ uint32_t end = w1 + n * 16;
+ if (end > maxReferencedEnd) maxReferencedEnd = end;
+ } else if (opcode == F3DEX2_G_SETTIMG || opcode == F3DEX2_G_MTX || opcode == F3DEX2_G_MOVEMEM) {
+ // We don't know exact sizes yet, but the offset itself must be in-bounds
+ // Add a generous margin (textures can be large)
+ if (w1 >= maxReferencedEnd) maxReferencedEnd = w1 + 0x800;
+ }
+ }
+ }
+
+ // Expand buffer from ROM if DLs reference data beyond the declared size
+ if (maxReferencedEnd > size) {
+ uint32_t expandedSize = maxReferencedEnd;
+ if (offset + expandedSize > buffer.size()) {
+ expandedSize = buffer.size() - offset;
+ }
+ entityData.assign(buffer.data() + offset, buffer.data() + offset + expandedSize);
+ }
+
+ // Collect standalone Mtx offsets from YAML (not reachable by DL walking)
+ std::vector<uint32_t> standaloneMtx;
+ auto standaloneMtxNode = node["standalone_mtx"];
+ if (standaloneMtxNode && standaloneMtxNode.IsSequence()) {
+ for (size_t i = 0; i < standaloneMtxNode.size(); i++) {
+ standaloneMtx.push_back(standaloneMtxNode[i].as<uint32_t>());
+ }
+ }
+
+ auto data = std::make_shared<PM64EntityGfxData>(std::move(entityData), std::move(collectedDLs));
+ data->mStandaloneMtx = std::move(standaloneMtx);
+ return data;
+}
+
+// Export a display list as an OTR DisplayList resource
+static void ExportEntityDisplayList(const std::string& entityName, const PM64EntityDisplayListInfo& dlInfo) {
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/dlist_%X", entityName.c_str(), dlInfo.offset);
+ std::string path = pathBuf;
+ std::string fullPath = Companion::Instance->RelativePath(path);
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::DisplayList, 0);
+
+ // GBI version byte (F3DEX2)
+ writer.Write(static_cast<int8_t>(GBIVersion::f3dex2));
+
+ // Pad to 8-byte alignment
+ while (writer.GetBaseAddress() % 8 != 0)
+ writer.Write(static_cast<int8_t>(0xFF));
+
+ // G_MARKER with resource hash
+ uint64_t hash = CRC64(fullPath.c_str());
+ writer.Write(static_cast<uint32_t>(G_MARKER << 24));
+ writer.Write(static_cast<uint32_t>(0xBEEFBEEF));
+ writer.Write(static_cast<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(hash & 0xFFFFFFFF));
+
+ // Write commands, converting to OTR format
+ for (size_t i = 0; i < dlInfo.commands.size(); i += 2) {
+ uint32_t w0 = dlInfo.commands[i];
+ uint32_t w1 = dlInfo.commands[i + 1];
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ if (opcode == F3DEX2_G_SETTIMG) {
+ // Convert to G_SETTIMG_OTR_HASH
+ char texPath[256];
+ snprintf(texPath, sizeof(texPath), "%s/tex_%X", entityName.c_str(), w1);
+ std::string fullTexPath = Companion::Instance->RelativePath(texPath);
+ uint64_t texHash = CRC64(fullTexPath.c_str());
+
+ uint32_t newW0 = (G_SETTIMG_OTR_HASH << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(texHash >> 32));
+ writer.Write(static_cast<uint32_t>(texHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_VTX) {
+ // Convert to G_VTX_OTR_HASH
+ char vtxPath[256];
+ snprintf(vtxPath, sizeof(vtxPath), "%s/vtx_%X", entityName.c_str(), w1);
+ std::string fullVtxPath = Companion::Instance->RelativePath(vtxPath);
+ uint64_t vtxHash = CRC64(fullVtxPath.c_str());
+
+ uint32_t newW0 = (G_VTX_OTR_HASH << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(vtxHash >> 32));
+ writer.Write(static_cast<uint32_t>(vtxHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_MTX) {
+ // Convert to G_MTX_OTR (hash-based)
+ char mtxPath[256];
+ snprintf(mtxPath, sizeof(mtxPath), "%s/mtx_%X", entityName.c_str(), w1);
+ std::string fullMtxPath = Companion::Instance->RelativePath(mtxPath);
+ uint64_t mtxHash = CRC64(fullMtxPath.c_str());
+
+ // Preserve flags from original w0 (push/nopush, load/mul, projection/modelview)
+ uint32_t newW0 = (G_MTX_OTR << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(mtxHash >> 32));
+ writer.Write(static_cast<uint32_t>(mtxHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_MOVEMEM) {
+ // Convert to G_MOVEMEM_OTR_HASH
+ char mmPath[256];
+ snprintf(mmPath, sizeof(mmPath), "%s/mm_%X", entityName.c_str(), w1);
+ std::string fullMmPath = Companion::Instance->RelativePath(mmPath);
+ uint64_t mmHash = CRC64(fullMmPath.c_str());
+
+ uint8_t index = w0 & 0xFF;
+ uint8_t mmOffset = ((w0 >> 8) & 0xFF) * 8;
+
+ writer.Write(static_cast<uint32_t>(G_MOVEMEM_OTR_HASH << 24));
+ writer.Write(static_cast<uint32_t>((index << 24) | (mmOffset << 16)));
+ writer.Write(static_cast<uint32_t>(mmHash >> 32));
+ writer.Write(static_cast<uint32_t>(mmHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_DL) {
+ // Convert to G_DL_OTR_HASH
+ char nestedPath[256];
+ snprintf(nestedPath, sizeof(nestedPath), "%s/dlist_%X", entityName.c_str(), w1);
+ std::string fullNestedPath = Companion::Instance->RelativePath(nestedPath);
+ uint64_t nestedHash = CRC64(fullNestedPath.c_str());
+
+ uint8_t pushFlag = (w0 >> 16) & 0x01;
+ uint32_t otrW0 = (0x31u << 24) | (pushFlag << 16);
+ writer.Write(otrW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(nestedHash >> 32));
+ writer.Write(static_cast<uint32_t>(nestedHash & 0xFFFFFFFF));
+ } else {
+ // Standard 8-byte command
+ writer.Write(w0);
+ writer.Write(w1);
+ }
+ }
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> data(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, data);
+}
+
+// Export vertex data as a Vertex resource (V1 format with float ob[])
+static void ExportVertexResource_Entity(const std::string& entityName, const uint8_t* data,
+ uint32_t offset, uint32_t size, uint32_t totalSize) {
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/vtx_%X", entityName.c_str(), offset);
+ std::string path = pathBuf;
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Vertex, 0);
+
+ // Write vertex count and per-vertex data (read from raw BE ROM data)
+ uint32_t count = size / 16;
+ writer.Write(count);
+ for (uint32_t i = 0; i < count; i++) {
+ const uint8_t* src = data + offset + i * 16;
+ writer.Write(static_cast<int16_t>((src[0] << 8) | src[1])); // ob[0]
+ writer.Write(static_cast<int16_t>((src[2] << 8) | src[3])); // ob[1]
+ writer.Write(static_cast<int16_t>((src[4] << 8) | src[5])); // ob[2]
+ writer.Write(static_cast<uint16_t>((src[6] << 8) | src[7])); // flag
+ writer.Write(static_cast<int16_t>((src[8] << 8) | src[9])); // tc[0]
+ writer.Write(static_cast<int16_t>((src[10] << 8) | src[11])); // tc[1]
+ writer.Write(src[12]); writer.Write(src[13]); writer.Write(src[14]); writer.Write(src[15]); // cn[4]
+ }
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> fileData(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, fileData);
+}
+
+// Map N64 fmt/siz to Torch TextureType enum value
+static uint32_t N64FmtSizToTextureType(uint32_t fmt, uint32_t siz) {
+ switch (fmt) {
+ case 0: // G_IM_FMT_RGBA
+ return (siz == 3) ? 1 : 2; // RGBA32bpp or RGBA16bpp
+ case 2: // G_IM_FMT_CI
+ return (siz == 0) ? 3 : 4; // Palette4bpp or Palette8bpp
+ case 4: // G_IM_FMT_I
+ return (siz == 0) ? 5 : 6; // Grayscale4bpp or Grayscale8bpp
+ case 3: // G_IM_FMT_IA
+ if (siz == 0) return 7; // GrayscaleAlpha4bpp
+ if (siz == 1) return 8; // GrayscaleAlpha8bpp
+ return 9; // GrayscaleAlpha16bpp
+ default:
+ return 2; // Default to RGBA16bpp
+ }
+}
+
+// Export texture/palette data as a Texture resource (V1 format)
+// Fast3D interpreter reads pixel/palette data as BE byte pairs — keep raw ROM byte order.
+static void ExportTextureResource(const std::string& entityName, const uint8_t* data,
+ uint32_t offset, uint32_t size, const char* prefix,
+ uint32_t settimgW0) {
+ if (offset + size > 0x100000) return; // Sanity check
+
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/%s_%X", entityName.c_str(), prefix, offset);
+ std::string path = pathBuf;
+
+ // Extract format info from the G_SETTIMG w0 word
+ uint32_t fmt = (settimgW0 >> 21) & 0x7;
+ uint32_t siz = (settimgW0 >> 19) & 0x3;
+ uint32_t width = (settimgW0 & 0xFFF) + 1;
+
+ // Compute height from data size and pixel format
+ uint32_t bitsPerPixel;
+ switch (siz) {
+ case 0: bitsPerPixel = 4; break;
+ case 1: bitsPerPixel = 8; break;
+ case 2: bitsPerPixel = 16; break;
+ case 3: bitsPerPixel = 32; break;
+ default: bitsPerPixel = 16; break;
+ }
+ uint32_t bytesPerRow = (width * bitsPerPixel + 7) / 8;
+ uint32_t height = (bytesPerRow > 0) ? (size / bytesPerRow) : 1;
+ if (height == 0) height = 1;
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Texture, 1);
+ writer.Write(N64FmtSizToTextureType(fmt, siz)); // Type
+ writer.Write(width); // Width
+ writer.Write(height); // Height
+ writer.Write(static_cast<uint32_t>(0)); // Flags
+ writer.Write(1.0f); // HByteScale
+ writer.Write(1.0f); // VPixelScale
+ writer.Write(static_cast<uint32_t>(size)); // ImageDataSize
+ writer.Write(const_cast<char*>(reinterpret_cast<const char*>(data + offset)), size);
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> fileData(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, fileData);
+}
+
+// Export matrix data as a Blob resource — convert N64 fixed-point to float[4][4]
+static void ExportMatrixBlob(const std::string& entityName, const uint8_t* data,
+ uint32_t offset) {
+ const uint32_t MTX_SIZE = 64; // N64 Mtx is 64 bytes (s15.16 interleaved)
+ std::vector<uint8_t> mtxData(data + offset, data + offset + MTX_SIZE);
+
+ // Byte-swap 32-bit words from BE
+ for (uint32_t i = 0; i + 4 <= MTX_SIZE; i += 4) {
+ uint32_t* v = reinterpret_cast<uint32_t*>(mtxData.data() + i);
+ *v = BSWAP32(*v);
+ }
+
+ // Decode interleaved integer/fraction parts to float[4][4]
+ int32_t* addr = reinterpret_cast<int32_t*>(mtxData.data());
+ float matrix[4][4];
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < 2; j++) {
+ int32_t int_part = addr[i * 2 + j];
+ uint32_t frac_part = addr[8 + i * 2 + j];
+ matrix[i][j * 2] = (int32_t)((int_part & 0xFFFF0000) | (frac_part >> 16)) / 65536.0f;
+ matrix[i][j * 2 + 1] = (int32_t)((int_part << 16) | (frac_part & 0xFFFF)) / 65536.0f;
+ }
+ }
+
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/mtx_%X", entityName.c_str(), offset);
+ std::string path = pathBuf;
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(MTX_SIZE));
+ writer.Write(reinterpret_cast<char*>(matrix), MTX_SIZE);
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> fileData(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, fileData);
+}
+
+// Export G_MOVEMEM data (lights, viewports) as a Blob resource
+static void ExportMovememBlob(const std::string& entityName, const uint8_t* data,
+ uint32_t offset, uint32_t size, uint8_t index) {
+ std::vector<uint8_t> mmData(data + offset, data + offset + size);
+
+ // Viewport data has s16 fields that need byte-swap
+ if (index == 0x08) { // G_MV_VIEWPORT
+ for (uint32_t i = 0; i + 2 <= size; i += 2) {
+ uint16_t* v = reinterpret_cast<uint16_t*>(mmData.data() + i);
+ *v = BSWAP16(*v);
+ }
+ }
+
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/mm_%X", entityName.c_str(), offset);
+ std::string path = pathBuf;
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(size));
+ writer.Write(reinterpret_cast<char*>(mmData.data()), size);
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> fileData(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, fileData);
+}
+
+ExportResult PM64EntityGfxBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto entityData = std::static_pointer_cast<PM64EntityGfxData>(raw);
+
+ // Extract entity name from entry path
+ std::string entityName = entryName;
+ size_t lastSlash = entryName.rfind('/');
+ if (lastSlash != std::string::npos) {
+ entityName = entryName.substr(lastSlash + 1);
+ }
+
+ // Collect all vertex, texture, matrix, and movemem offsets from display lists
+ std::unordered_set<uint32_t> vtxOffsets;
+ std::unordered_map<uint32_t, uint32_t> texInfo; // offset → G_SETTIMG w0
+ std::unordered_set<uint32_t> mtxOffsets;
+ std::unordered_map<uint32_t, uint32_t> mmInfo; // offset → w0
+
+ for (const auto& dl : entityData->mDisplayLists) {
+ for (size_t i = 0; i < dl.commands.size(); i += 2) {
+ uint32_t w0 = dl.commands[i];
+ uint32_t w1 = dl.commands[i + 1];
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ if (opcode == F3DEX2_G_VTX) {
+ vtxOffsets.insert(w1);
+ } else if (opcode == F3DEX2_G_SETTIMG) {
+ if (texInfo.find(w1) == texInfo.end()) {
+ texInfo[w1] = w0;
+ }
+ } else if (opcode == F3DEX2_G_MTX) {
+ mtxOffsets.insert(w1);
+ } else if (opcode == F3DEX2_G_MOVEMEM) {
+ if (mmInfo.find(w1) == mmInfo.end()) {
+ mmInfo[w1] = w0;
+ }
+ }
+ }
+ }
+
+ // Export vertex blobs
+ std::vector<uint32_t> sortedVtxOffsets(vtxOffsets.begin(), vtxOffsets.end());
+ std::sort(sortedVtxOffsets.begin(), sortedVtxOffsets.end());
+
+ for (size_t i = 0; i < sortedVtxOffsets.size(); i++) {
+ uint32_t vtxOff = sortedVtxOffsets[i];
+ // Find max vertex count from G_VTX commands referencing this offset
+ uint32_t vtxSize = 0;
+ for (const auto& dl : entityData->mDisplayLists) {
+ for (size_t j = 0; j < dl.commands.size(); j += 2) {
+ uint32_t w0 = dl.commands[j];
+ uint32_t w1 = dl.commands[j + 1];
+ uint8_t op = (w0 >> 24) & 0xFF;
+ if (op == F3DEX2_G_VTX && w1 == vtxOff) {
+ uint32_t n = (w0 >> 12) & 0xFF;
+ uint32_t candidateSize = n * 16;
+ if (candidateSize > vtxSize) vtxSize = candidateSize;
+ }
+ }
+ }
+ if (vtxSize == 0) vtxSize = 256;
+ if (vtxOff + vtxSize <= entityData->mBuffer.size()) {
+ ExportVertexResource_Entity(entityName, entityData->mBuffer.data(), vtxOff, vtxSize, entityData->mBuffer.size());
+ }
+ }
+
+ // Export texture resources
+ std::vector<uint32_t> sortedTexOffsets;
+ for (const auto& [off, w0] : texInfo) {
+ sortedTexOffsets.push_back(off);
+ }
+ std::sort(sortedTexOffsets.begin(), sortedTexOffsets.end());
+
+ for (size_t i = 0; i < sortedTexOffsets.size(); i++) {
+ uint32_t texOff = sortedTexOffsets[i];
+ uint32_t texSize;
+ if (i + 1 < sortedTexOffsets.size()) {
+ texSize = sortedTexOffsets[i + 1] - texOff;
+ } else {
+ // Last texture - find next known structure after it
+ uint32_t nextOff = entityData->mBuffer.size();
+ for (const auto& dl : entityData->mDisplayLists) {
+ if (dl.offset > texOff && dl.offset < nextOff) {
+ nextOff = dl.offset;
+ }
+ }
+ for (uint32_t vo : sortedVtxOffsets) {
+ if (vo > texOff && vo < nextOff) {
+ nextOff = vo;
+ }
+ }
+ for (uint32_t mo : mtxOffsets) {
+ if (mo > texOff && mo < nextOff) {
+ nextOff = mo;
+ }
+ }
+ texSize = nextOff - texOff;
+ }
+ if (texOff + texSize <= entityData->mBuffer.size()) {
+ ExportTextureResource(entityName, entityData->mBuffer.data(), texOff, texSize, "tex", texInfo[texOff]);
+ }
+ }
+
+ // Export matrix blobs (from DL references)
+ for (uint32_t mtxOff : mtxOffsets) {
+ if (mtxOff + 64 <= entityData->mBuffer.size()) {
+ ExportMatrixBlob(entityName, entityData->mBuffer.data(), mtxOff);
+ }
+ }
+
+ // Export standalone matrix blobs (not reachable by DL walking, e.g. Chest lid, Padlock shackle)
+ for (uint32_t mtxOff : entityData->mStandaloneMtx) {
+ if (mtxOffsets.count(mtxOff) == 0 && mtxOff + 64 <= entityData->mBuffer.size()) {
+ ExportMatrixBlob(entityName, entityData->mBuffer.data(), mtxOff);
+ }
+ }
+
+ // Export movemem data blobs
+ for (const auto& [mmOff, mmW0] : mmInfo) {
+ uint32_t sizeField = (mmW0 >> 19) & 0x1F;
+ uint32_t dataSize = (sizeField + 1) * 8;
+ uint8_t index = mmW0 & 0xFF;
+ if (mmOff + dataSize <= entityData->mBuffer.size()) {
+ ExportMovememBlob(entityName, entityData->mBuffer.data(), mmOff, dataSize, index);
+ }
+ }
+
+ // Export each display list
+ for (const auto& dl : entityData->mDisplayLists) {
+ ExportEntityDisplayList(entityName, dl);
+ }
+
+ // Write main blob (entire entity data)
+ auto writer = LUS::BinaryWriter();
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(entityData->mBuffer.size()));
+ writer.Write(reinterpret_cast<char*>(entityData->mBuffer.data()), entityData->mBuffer.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64EntityGfxHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ // Header generation handled by tools/extract-entity-offsets.py
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/EntityGfxFactory.h b/src/factories/pm64/EntityGfxFactory.h
new file mode 100644
index 0000000..c64712b
--- /dev/null
+++ b/src/factories/pm64/EntityGfxFactory.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+#include <vector>
+
+struct PM64EntityDisplayListInfo {
+ uint32_t offset;
+ std::vector<uint32_t> commands;
+};
+
+class PM64EntityGfxData : public IParsedData {
+public:
+ std::vector<uint8_t> mBuffer;
+ std::vector<PM64EntityDisplayListInfo> mDisplayLists;
+ std::vector<uint32_t> mStandaloneMtx; // Mtx offsets not reachable by DL walking
+
+ PM64EntityGfxData(std::vector<uint8_t>&& buffer, std::vector<PM64EntityDisplayListInfo>&& displayLists)
+ : mBuffer(std::move(buffer)), mDisplayLists(std::move(displayLists)) {
+ }
+};
+
+class PM64EntityGfxBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64EntityGfxHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64EntityGfxFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64EntityGfxHeaderExporter)
+ REGISTER(Binary, PM64EntityGfxBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/ImgFXAnimFactory.cpp b/src/factories/pm64/ImgFXAnimFactory.cpp
new file mode 100644
index 0000000..503e558
--- /dev/null
+++ b/src/factories/pm64/ImgFXAnimFactory.cpp
@@ -0,0 +1,107 @@
+#include "ImgFXAnimFactory.h"
+#include "Companion.h"
+#include "spdlog/spdlog.h"
+
+// ImgFX animation blob layout (produced by this factory):
+//
+// [0x00] u32 n64KeyframesOffset (original segment-relative, for vertex fixup)
+// [0x04] u32 n64GfxOffset (original segment-relative, unused on port)
+// [0x08] u16 vtxCount
+// [0x0A] u16 gfxCount
+// [0x0C] u16 keyframesCount
+// [0x0E] u16 flags
+// [0x10] Keyframe data (keyframesCount * vtxCount * 12 bytes, positions byte-swapped)
+// [0x10 + keyframeDataSize] GFX data (gfxCount * 8 bytes, N64 Gfx commands word-swapped)
+
+std::optional<std::shared_ptr<IParsedData>> PM64ImgFXAnimFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto segmentBase = GetSafeNode<uint32_t>(node, "offset");
+ auto headerOffset = GetSafeNode<uint32_t>(node, "header_offset");
+
+ uint32_t headerRomAddr = segmentBase + headerOffset;
+
+ if (headerRomAddr + 16 > buffer.size()) {
+ SPDLOG_ERROR("PM64:IMGFX_ANIM: Header at 0x{:X} exceeds buffer size 0x{:X}",
+ headerRomAddr, buffer.size());
+ return std::nullopt;
+ }
+
+ // Read 16-byte N64 header (big-endian)
+ uint8_t* hdr = buffer.data() + headerRomAddr;
+ uint32_t n64KeyframesOffset = BSWAP32(*(uint32_t*)(hdr + 0x00));
+ uint32_t n64GfxOffset = BSWAP32(*(uint32_t*)(hdr + 0x04));
+ uint16_t vtxCount = BSWAP16(*(uint16_t*)(hdr + 0x08));
+ uint16_t gfxCount = BSWAP16(*(uint16_t*)(hdr + 0x0A));
+ uint16_t keyframesCount = BSWAP16(*(uint16_t*)(hdr + 0x0C));
+ uint16_t flags = BSWAP16(*(uint16_t*)(hdr + 0x0E));
+
+ uint32_t keyframeDataSize = keyframesCount * vtxCount * 12; // sizeof(ImgFXVtx) = 0x0C
+ uint32_t gfxDataSize = gfxCount * 8; // sizeof(N64 Gfx) = 8
+
+ uint32_t keyframesRomAddr = segmentBase + n64KeyframesOffset;
+ uint32_t gfxRomAddr = segmentBase + n64GfxOffset;
+
+ if (keyframesRomAddr + keyframeDataSize > buffer.size()) {
+ SPDLOG_ERROR("PM64:IMGFX_ANIM: Keyframe data at 0x{:X} (size 0x{:X}) exceeds buffer",
+ keyframesRomAddr, keyframeDataSize);
+ return std::nullopt;
+ }
+ if (gfxRomAddr + gfxDataSize > buffer.size()) {
+ SPDLOG_ERROR("PM64:IMGFX_ANIM: GFX data at 0x{:X} (size 0x{:X}) exceeds buffer",
+ gfxRomAddr, gfxDataSize);
+ return std::nullopt;
+ }
+
+ // Build output blob: header (16) + keyframes + gfx
+ uint32_t blobSize = 16 + keyframeDataSize + gfxDataSize;
+ std::vector<uint8_t> blob(blobSize);
+
+ // Write header (already byte-swapped to native LE)
+ *(uint32_t*)(blob.data() + 0x00) = n64KeyframesOffset;
+ *(uint32_t*)(blob.data() + 0x04) = n64GfxOffset;
+ *(uint16_t*)(blob.data() + 0x08) = vtxCount;
+ *(uint16_t*)(blob.data() + 0x0A) = gfxCount;
+ *(uint16_t*)(blob.data() + 0x0C) = keyframesCount;
+ *(uint16_t*)(blob.data() + 0x0E) = flags;
+
+ // Copy and byte-swap keyframe data
+ // ImgFXVtx: s16 ob[3] (bytes 0-5, need swap), u8 tc[2] (bytes 6-7, no swap),
+ // s8 cn[3] (bytes 8-10, no swap), pad (byte 11, no swap)
+ uint8_t* kfSrc = buffer.data() + keyframesRomAddr;
+ uint8_t* kfDst = blob.data() + 16;
+ memcpy(kfDst, kfSrc, keyframeDataSize);
+ for (uint32_t i = 0; i + 12 <= keyframeDataSize; i += 12) {
+ uint16_t* v = reinterpret_cast<uint16_t*>(kfDst + i);
+ v[0] = BSWAP16(v[0]); // ob[0]
+ v[1] = BSWAP16(v[1]); // ob[1]
+ v[2] = BSWAP16(v[2]); // ob[2]
+ // bytes 6-11: u8/s8 fields, no swap needed
+ }
+
+ // Copy and byte-swap GFX data (N64 Gfx: two u32 words each)
+ uint8_t* gfxSrc = buffer.data() + gfxRomAddr;
+ uint8_t* gfxDst = blob.data() + 16 + keyframeDataSize;
+ memcpy(gfxDst, gfxSrc, gfxDataSize);
+ for (uint32_t i = 0; i + 8 <= gfxDataSize; i += 8) {
+ uint32_t* words = reinterpret_cast<uint32_t*>(gfxDst + i);
+ words[0] = BSWAP32(words[0]);
+ words[1] = BSWAP32(words[1]);
+ }
+
+ return std::make_shared<RawBuffer>(blob);
+}
+
+ExportResult PM64ImgFXAnimBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64ImgFXAnimHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/ImgFXAnimFactory.h b/src/factories/pm64/ImgFXAnimFactory.h
new file mode 100644
index 0000000..acf9284
--- /dev/null
+++ b/src/factories/pm64/ImgFXAnimFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64ImgFXAnimBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64ImgFXAnimHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64ImgFXAnimFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64ImgFXAnimHeaderExporter)
+ REGISTER(Binary, PM64ImgFXAnimBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/MapTextureFactory.cpp b/src/factories/pm64/MapTextureFactory.cpp
new file mode 100644
index 0000000..7ccf9aa
--- /dev/null
+++ b/src/factories/pm64/MapTextureFactory.cpp
@@ -0,0 +1,256 @@
+#include "MapTextureFactory.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include "spdlog/spdlog.h"
+
+// PM64 TextureHeader structure (0x30 bytes):
+// 0x00: name[32] - texture name string
+// 0x20: auxW (u16)
+// 0x22: mainW (u16)
+// 0x24: auxH (u16)
+// 0x26: mainH (u16)
+// 0x28: isVariant (u8)
+// 0x29: extraTiles (u8)
+// 0x2A: auxCombineType:6, auxCombineSubType:2 (u8)
+// 0x2B: auxFmt:4, mainFmt:4 (u8)
+// 0x2C: auxBitDepth:4, mainBitDepth:4 (u8)
+// 0x2D: auxWrapW:4, mainWrapW:4 (u8)
+// 0x2E: auxWrapH:4, mainWrapH:4 (u8)
+// 0x2F: filtering (u8)
+
+static constexpr size_t TEXTURE_HEADER_SIZE = 0x30;
+
+// Image format/bit depth constants (matching N64 GBI)
+enum ImgFmt {
+ G_IM_FMT_RGBA = 0,
+ G_IM_FMT_YUV = 1,
+ G_IM_FMT_CI = 2,
+ G_IM_FMT_IA = 3,
+ G_IM_FMT_I = 4,
+};
+
+enum ImgSiz {
+ G_IM_SIZ_4b = 0,
+ G_IM_SIZ_8b = 1,
+ G_IM_SIZ_16b = 2,
+ G_IM_SIZ_32b = 3,
+};
+
+enum ExtraTiles {
+ EXTRA_TILE_NONE = 0,
+ EXTRA_TILE_MIPMAPS = 1,
+ EXTRA_TILE_AUX_SAME_AS_MAIN = 2,
+ EXTRA_TILE_AUX_INDEPENDENT = 3,
+};
+
+static void ByteSwapTextureHeader(uint8_t* headerPtr) {
+ // Byte-swap u16 fields at offsets 0x20, 0x22, 0x24, 0x26
+ uint16_t* auxW = reinterpret_cast<uint16_t*>(headerPtr + 0x20);
+ uint16_t* mainW = reinterpret_cast<uint16_t*>(headerPtr + 0x22);
+ uint16_t* auxH = reinterpret_cast<uint16_t*>(headerPtr + 0x24);
+ uint16_t* mainH = reinterpret_cast<uint16_t*>(headerPtr + 0x26);
+
+ *auxW = BSWAP16(*auxW);
+ *mainW = BSWAP16(*mainW);
+ *auxH = BSWAP16(*auxH);
+ *mainH = BSWAP16(*mainH);
+
+ // Fix bitfield byte layout for little-endian.
+ // On N64 (big-endian), GCC lays out the first-declared bitfield in the UPPER bits.
+ // On LE (ARM64/x86), the first-declared bitfield occupies the LOWER bits.
+ // The TextureHeader struct has paired bitfields within single bytes:
+ // 0x2A: auxCombineType:6, auxCombineSubType:2
+ // 0x2B: auxFmt:4, mainFmt:4
+ // 0x2C: auxBitDepth:4, mainBitDepth:4
+ // 0x2D: auxWrapW:4, mainWrapW:4
+ // 0x2E: auxWrapH:4, mainWrapH:4
+ // Without rearranging, the LE struct reads mainFmt where auxFmt should be (and vice versa).
+
+ // 0x2A: 6:2 split — N64 byte = (combineType << 2) | combineSubType
+ // LE needs: combineType | (combineSubType << 6)
+ uint8_t b = headerPtr[0x2A];
+ headerPtr[0x2A] = ((b >> 2) & 0x3F) | ((b & 0x03) << 6);
+
+ // 0x2B-0x2E: 4:4 splits — swap nibbles
+ for (int i = 0x2B; i <= 0x2E; i++) {
+ b = headerPtr[i];
+ headerPtr[i] = ((b & 0x0F) << 4) | ((b >> 4) & 0x0F);
+ }
+}
+
+// Calculate raster size for a texture (including mipmaps if present)
+static uint32_t CalculateRasterSize(uint16_t width, uint16_t height, uint8_t bitDepth, uint8_t extraTiles) {
+ uint32_t rasterSize = width * height;
+
+ // Compute mipmaps size if present
+ if (extraTiles == EXTRA_TILE_MIPMAPS) {
+ if (bitDepth == G_IM_SIZ_4b) {
+ int d = 2;
+ while (width / d >= 16 && height / d > 0) {
+ rasterSize += (width / d) * (height / d);
+ d *= 2;
+ }
+ } else if (bitDepth == G_IM_SIZ_8b) {
+ int d = 2;
+ while (width / d >= 8 && height / d > 0) {
+ rasterSize += (width / d) * (height / d);
+ d *= 2;
+ }
+ } else if (bitDepth == G_IM_SIZ_16b) {
+ int d = 2;
+ while (width / d >= 4 && height / d > 0) {
+ rasterSize += (width / d) * (height / d);
+ d *= 2;
+ }
+ } else if (bitDepth == G_IM_SIZ_32b) {
+ int d = 2;
+ while (width / d >= 2 && height / d > 0) {
+ rasterSize += (width / d) * (height / d);
+ d *= 2;
+ }
+ }
+ }
+
+ // Scale by bit depth
+ if (bitDepth == G_IM_SIZ_4b) {
+ rasterSize /= 2;
+ } else if (bitDepth == G_IM_SIZ_16b) {
+ rasterSize *= 2;
+ } else if (bitDepth == G_IM_SIZ_32b) {
+ rasterSize *= 4;
+ }
+
+ return rasterSize;
+}
+
+// Calculate palette size for a texture
+static uint32_t CalculatePaletteSize(uint8_t fmt, uint8_t bitDepth) {
+ if (fmt == G_IM_FMT_CI) {
+ return (bitDepth == G_IM_SIZ_8b) ? 0x200 : 0x20;
+ }
+ return 0;
+}
+
+static void ByteSwapAllTextureHeaders(uint8_t* data, size_t size) {
+ size_t offset = 0;
+
+ while (offset + TEXTURE_HEADER_SIZE <= size) {
+ uint8_t* headerPtr = data + offset;
+
+ // Check if this looks like a valid texture header (name should be ASCII)
+ bool validName = true;
+ for (int i = 0; i < 32 && headerPtr[i] != 0; i++) {
+ if (headerPtr[i] < 0x20 || headerPtr[i] > 0x7E) {
+ validName = false;
+ break;
+ }
+ }
+
+ if (!validName) {
+ // End of texture list or invalid data
+ break;
+ }
+
+ // Read header fields (still in big-endian at this point)
+ uint16_t mainW = (headerPtr[0x22] << 8) | headerPtr[0x23];
+ uint16_t mainH = (headerPtr[0x26] << 8) | headerPtr[0x27];
+ uint16_t auxW = (headerPtr[0x20] << 8) | headerPtr[0x21];
+ uint16_t auxH = (headerPtr[0x24] << 8) | headerPtr[0x25];
+ uint8_t extraTiles = headerPtr[0x29];
+ uint8_t mainBitDepth = headerPtr[0x2C] & 0x0F;
+ uint8_t mainFmt = headerPtr[0x2B] & 0x0F;
+ uint8_t auxBitDepth = (headerPtr[0x2C] >> 4) & 0x0F;
+ uint8_t auxFmt = (headerPtr[0x2B] >> 4) & 0x0F;
+
+ // Validate dimensions
+ if (mainW == 0 || mainH == 0 || mainW > 1024 || mainH > 1024) {
+ break;
+ }
+
+ // Byte-swap this header
+ ByteSwapTextureHeader(headerPtr);
+
+ // Calculate texture data size to skip to next header
+ uint32_t rasterSize = CalculateRasterSize(mainW, mainH, mainBitDepth, extraTiles);
+ uint32_t paletteSize = CalculatePaletteSize(mainFmt, mainBitDepth);
+
+ uint32_t auxRasterSize = 0;
+ uint32_t auxPaletteSize = 0;
+ if (extraTiles == EXTRA_TILE_AUX_INDEPENDENT) {
+ auxRasterSize = CalculateRasterSize(auxW, auxH, auxBitDepth, EXTRA_TILE_NONE);
+ auxPaletteSize = CalculatePaletteSize(auxFmt, auxBitDepth);
+ }
+
+ // Move to next texture entry
+ offset += TEXTURE_HEADER_SIZE + rasterSize + paletteSize + auxRasterSize + auxPaletteSize;
+ }
+
+ SPDLOG_DEBUG("Byte-swapped texture headers up to offset 0x{:X}", offset);
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64MapTextureFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+
+ // Check if compressed (YAY0)
+ auto compressionType = Decompressor::GetCompressionType(buffer, offset);
+
+ std::vector<uint8_t> textureData;
+
+ if (compressionType == CompressionType::YAY0) {
+ auto decoded = Decompressor::Decode(buffer, offset, CompressionType::YAY0);
+ if (!decoded || decoded->size == 0) {
+ SPDLOG_ERROR("Failed to decompress YAY0 map texture data at offset 0x{:X}", offset);
+ return std::nullopt;
+ }
+
+ textureData.assign(decoded->data, decoded->data + decoded->size);
+ } else {
+ // Uncompressed - read raw data
+ // For uncompressed textures, we need to determine the size from somewhere
+ // Usually specified in YAML or we read until we hit invalid data
+ auto sizeOpt = GetSafeNode<size_t>(node, "size", 0);
+ size_t size = sizeOpt;
+
+ if (size == 0) {
+ // Try to auto-detect size by scanning for valid texture headers
+ // This is a fallback - normally size should be in YAML
+ size = 0x40000; // Max reasonable size
+ }
+
+ if (offset + size > buffer.size()) {
+ size = buffer.size() - offset;
+ }
+
+ textureData.assign(buffer.begin() + offset, buffer.begin() + offset + size);
+ }
+
+ // Byte-swap all texture headers in the data
+ ByteSwapAllTextureHeaders(textureData.data(), textureData.size());
+
+ return std::make_shared<RawBuffer>(textureData);
+}
+
+ExportResult PM64MapTextureBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ // Write as Blob type - game loads as raw binary
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64MapTextureHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/MapTextureFactory.h b/src/factories/pm64/MapTextureFactory.h
new file mode 100644
index 0000000..ed7a1ce
--- /dev/null
+++ b/src/factories/pm64/MapTextureFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64MapTextureBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64MapTextureHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64MapTextureFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64MapTextureHeaderExporter)
+ REGISTER(Binary, PM64MapTextureBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/ShapeFactory.cpp b/src/factories/pm64/ShapeFactory.cpp
new file mode 100644
index 0000000..fb1dc52
--- /dev/null
+++ b/src/factories/pm64/ShapeFactory.cpp
@@ -0,0 +1,736 @@
+#include "ShapeFactory.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include "spdlog/spdlog.h"
+#include <unordered_set>
+#include <sstream>
+#include <cstring>
+#include "n64/CommandMacros.h"
+#include "factories/DisplayListOverrides.h"
+#include "n64/gbi-otr.h"
+#include "strhash64/StrHash64.h"
+
+// PM64 shape file structures (matching model.h):
+// ShapeFileHeader (0x20 bytes):
+// 0x00: root (ModelNode*)
+// 0x04: vertexTable (Vtx_t*)
+// 0x08: modelNames (char**)
+// 0x0C: colliderNames (char**)
+// 0x10: zoneNames (char**)
+// 0x14: pad[0xC]
+//
+// ModelNode (0x14 bytes):
+// 0x00: type (s32)
+// 0x04: displayData (ModelDisplayData*)
+// 0x08: numProperties (s32)
+// 0x0C: propertyList (ModelNodeProperty*)
+// 0x10: groupData (ModelGroupData*)
+//
+// ModelGroupData (0x14 bytes):
+// 0x00: transformMatrix (Mtx*)
+// 0x04: lightingGroup (Lightsn*)
+// 0x08: numLights (s32)
+// 0x0C: numChildren (s32)
+// 0x10: childList (ModelNode**)
+//
+// ModelDisplayData (0x08 bytes):
+// 0x00: displayList (Gfx*)
+// 0x04: unk_04 (4 bytes)
+//
+// ModelNodeProperty (0x0C bytes):
+// 0x00: key (s32)
+// 0x04: dataType (s32)
+// 0x08: data (union: s32/f32/void*)
+
+// Base address used for N64 virtual address to offset conversion
+// This is computed from the header's root pointer assuming root is at offset 0x20
+static uint32_t gShapeBaseAddr = 0;
+
+// Vertex table offset in shape file - used for converting G_VTX offsets to vertex-table-relative
+static uint32_t gVertexTableOffset = 0;
+
+// Track visited offsets to prevent infinite recursion from cycles and to exclude from vertex byte-swapping
+static std::unordered_set<uint32_t> gVisitedNodes;
+static std::unordered_set<uint32_t> gVisitedGroups;
+static std::unordered_set<uint32_t> gVisitedMatrices;
+static std::unordered_set<uint32_t> gVisitedDisplayLists;
+static std::unordered_set<uint32_t> gVisitedDisplayData;
+static std::unordered_set<uint32_t> gVisitedProperties;
+
+// Collected display lists during parsing
+static std::vector<PM64DisplayListInfo>* gCollectedDisplayLists = nullptr;
+
+// Convert N64 virtual address to file offset
+static uint32_t N64AddrToOffset(uint32_t addr) {
+ if (addr == 0) return 0;
+
+ // Check if it looks like an N64 virtual address (segment in high byte)
+ if (addr >= 0x80000000) {
+ // It's an N64 address - convert using base
+ if (gShapeBaseAddr == 0) {
+ // Fallback: mask off the segment and use as offset
+ return addr & 0x00FFFFFF;
+ }
+ if (addr >= gShapeBaseAddr) {
+ return addr - gShapeBaseAddr;
+ }
+ // Address is below base - might be in a different segment, try masking
+ return addr & 0x00FFFFFF;
+ }
+
+ // Small value - assume it's already a file offset
+ return addr;
+}
+
+static bool IsValidOffset(uint32_t offset, size_t size) {
+ return offset > 0 && offset < size;
+}
+
+// F3DEX2 GBI opcodes used in shape display lists
+#define F3DEX2_G_ENDDL 0xDF
+#define F3DEX2_G_VTX 0x01
+#define F3DEX2_G_DL 0xDE
+#define F3DEX2_G_SETTIMG 0xFD
+
+// Check if an opcode is a valid F3DEX2 GBI command.
+// Valid ranges: 0x00-0x07 (geometry), 0xD7-0xDF (matrix/mode), 0xE4-0xFF (RDP).
+static bool IsValidF3DEX2Opcode(uint8_t opcode) {
+ if (opcode <= 0x07) return true; // G_NOOP..G_QUAD
+ if (opcode >= 0xD7 && opcode <= 0xDF) return true; // G_TEXTURE..G_ENDDL
+ if (opcode >= 0xE4) return true; // G_TEXRECT..G_SETCIMG
+ return false;
+}
+
+// Byte-swap display list commands, convert embedded N64 addresses to file offsets,
+// and collect the display list for separate resource export
+static void ByteSwapDisplayList(uint8_t* data, uint32_t offset, size_t size) {
+ if (!IsValidOffset(offset, size - 8)) return;
+ if (gVisitedDisplayLists.count(offset)) return; // Already processed
+ gVisitedDisplayLists.insert(offset);
+
+ uint8_t* ptr = data + offset;
+ uint8_t* endPtr = data + size;
+
+ // Collect this display list's commands
+ PM64DisplayListInfo dlInfo;
+ dlInfo.offset = offset;
+
+ while (ptr + 8 <= endPtr) {
+ uint32_t* words = reinterpret_cast<uint32_t*>(ptr);
+
+ // Read big-endian words
+ uint32_t w0 = BSWAP32(words[0]);
+ uint32_t w1 = BSWAP32(words[1]);
+
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ // Stop if we hit a non-F3DEX2 opcode — we've overrun past the display list
+ // into adjacent data (e.g., string data, vertex data, padding).
+ if (!IsValidF3DEX2Opcode(opcode)) {
+ SPDLOG_WARN("DL at 0x{:X}: invalid opcode 0x{:02X} at offset 0x{:X}, stopping",
+ offset, opcode, (uint32_t)(ptr - data));
+ break;
+ }
+
+ // Handle G_VTX - convert vertex address to vertex-table-relative offset
+ // With GBI_FLOATS, Vtx is 24 bytes (not 16), so convert byte offset accordingly
+ if (opcode == F3DEX2_G_VTX) {
+ // w1 contains the N64 vertex address - convert to file offset first
+ uint32_t vtxFileOffset = N64AddrToOffset(w1);
+ // Then convert to vertex-table-relative offset with 16->24 byte stride conversion
+ uint32_t vtxByteOffset;
+ if (gVertexTableOffset > 0 && vtxFileOffset >= gVertexTableOffset) {
+ vtxByteOffset = vtxFileOffset - gVertexTableOffset;
+ } else {
+ vtxByteOffset = vtxFileOffset;
+ }
+ uint32_t vtxIndex = vtxByteOffset / 16;
+ w1 = vtxIndex * 24; // sizeof(Vtx) with GBI_FLOATS = 24
+ }
+
+ // Handle G_SETTIMG - convert texture address to file offset
+ // Note: Shape textures may be loaded separately, but we convert anyway for safety
+ if (opcode == F3DEX2_G_SETTIMG) {
+ // w1 contains the N64 texture address
+ w1 = N64AddrToOffset(w1);
+ }
+
+ // Handle G_DL - convert display list address to file offset and recurse
+ if (opcode == F3DEX2_G_DL) {
+ uint32_t dlOffset = N64AddrToOffset(w1);
+ w1 = dlOffset;
+ // Recursively process the referenced display list
+ ByteSwapDisplayList(data, dlOffset, size);
+ }
+
+ // Write the byte-swapped (now little-endian) words back
+ words[0] = w0;
+ words[1] = w1;
+
+ // Collect the command for OTR export
+ dlInfo.commands.push_back(w0);
+ dlInfo.commands.push_back(w1);
+
+ // Stop at G_ENDDL
+ if (opcode == F3DEX2_G_ENDDL) {
+ break;
+ }
+
+ ptr += 8; // Move to next Gfx command (8 bytes each)
+ }
+
+ // Add to collected display lists
+ if (gCollectedDisplayLists && !dlInfo.commands.empty()) {
+ gCollectedDisplayLists->push_back(std::move(dlInfo));
+ }
+}
+
+// Property key for texture names - these store N64 addresses to strings
+#define MODEL_PROP_KEY_TEXTURE_NAME 0x5E
+
+static void ByteSwapModelNodeProperty(uint8_t* data, uint32_t offset, size_t size) {
+ if (!IsValidOffset(offset, size - 0xC)) return;
+
+ uint32_t* prop = reinterpret_cast<uint32_t*>(data + offset);
+ int32_t key = static_cast<int32_t>(BSWAP32(prop[0]));
+ prop[0] = static_cast<uint32_t>(key);
+ prop[1] = BSWAP32(prop[1]); // dataType
+
+ // For texture name properties, convert N64 address to file offset
+ if (key == MODEL_PROP_KEY_TEXTURE_NAME) {
+ uint32_t dataAddr = BSWAP32(prop[2]);
+ uint32_t strOffset = N64AddrToOffset(dataAddr);
+ prop[2] = strOffset;
+ } else {
+ prop[2] = BSWAP32(prop[2]); // data (scalar value)
+ }
+}
+
+static void ByteSwapModelDisplayData(uint8_t* data, uint32_t offset, size_t size) {
+ if (!IsValidOffset(offset, size - 0x8)) return;
+
+ // Track this offset so vertex byte-swapping skips it
+ gVisitedDisplayData.insert(offset);
+
+ uint32_t* display = reinterpret_cast<uint32_t*>(data + offset);
+ // displayList is a pointer - convert N64 address to offset
+ uint32_t dlAddr = BSWAP32(display[0]);
+ uint32_t dlOffset = N64AddrToOffset(dlAddr);
+
+ display[0] = dlOffset;
+ display[1] = BSWAP32(display[1]); // unk_04
+
+ // Byte-swap the display list commands themselves and collect for export
+ if (IsValidOffset(dlOffset, size)) {
+ ByteSwapDisplayList(data, dlOffset, size);
+ }
+}
+
+static void ByteSwapModelGroupData(uint8_t* data, uint32_t offset, size_t size);
+static void ByteSwapModelNode(uint8_t* data, uint32_t offset, size_t size);
+
+static void ByteSwapModelGroupData(uint8_t* data, uint32_t offset, size_t size) {
+ if (!IsValidOffset(offset, size - 0x14)) return;
+ if (gVisitedGroups.count(offset)) return; // Already processed
+ gVisitedGroups.insert(offset);
+
+ uint32_t* group = reinterpret_cast<uint32_t*>(data + offset);
+
+ // Read and convert N64 addresses to offsets
+ uint32_t transformMatrixAddr = BSWAP32(group[0]);
+ uint32_t lightingGroupAddr = BSWAP32(group[1]);
+ int32_t numLights = static_cast<int32_t>(BSWAP32(group[2]));
+ int32_t numChildren = static_cast<int32_t>(BSWAP32(group[3]));
+ uint32_t childListAddr = BSWAP32(group[4]);
+
+ // Convert N64 addresses to file offsets
+ uint32_t transformMatrix = N64AddrToOffset(transformMatrixAddr);
+ uint32_t lightingGroup = N64AddrToOffset(lightingGroupAddr);
+ uint32_t childList = N64AddrToOffset(childListAddr);
+
+ group[0] = transformMatrix;
+ group[1] = lightingGroup;
+ group[2] = static_cast<uint32_t>(numLights);
+ group[3] = static_cast<uint32_t>(numChildren);
+ group[4] = childList;
+
+ // Convert N64 fixed-point matrix (s15.16 interleaved) to float[4][4]
+ // Multiple groups can share the same matrix — only convert once
+ if (IsValidOffset(transformMatrix, size - 0x40) && !gVisitedMatrices.count(transformMatrix)) {
+ gVisitedMatrices.insert(transformMatrix);
+ uint32_t* raw = reinterpret_cast<uint32_t*>(data + transformMatrix);
+ // First byte-swap all 16 words from BE
+ for (int i = 0; i < 16; i++) {
+ raw[i] = BSWAP32(raw[i]);
+ }
+ // Decode interleaved integer/fraction parts to float
+ int32_t* addr = reinterpret_cast<int32_t*>(raw);
+ float matrix[4][4];
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < 2; j++) {
+ int32_t int_part = addr[i * 2 + j];
+ uint32_t frac_part = addr[8 + i * 2 + j];
+ matrix[i][j * 2] = (int32_t)((int_part & 0xFFFF0000) | (frac_part >> 16)) / 65536.0f;
+ matrix[i][j * 2 + 1] = (int32_t)((int_part << 16) | (frac_part & 0xFFFF)) / 65536.0f;
+ }
+ }
+ memcpy(raw, matrix, sizeof(matrix));
+ }
+
+ // Byte-swap child list and recurse into child nodes
+ // Sanity check: numChildren should be reasonable (< 1000)
+ if (numChildren > 0 && numChildren < 1000 && IsValidOffset(childList, size - (numChildren * 4))) {
+ uint32_t* children = reinterpret_cast<uint32_t*>(data + childList);
+ for (int i = 0; i < numChildren; i++) {
+ uint32_t childAddr = BSWAP32(children[i]);
+ uint32_t childOffset = N64AddrToOffset(childAddr);
+ children[i] = childOffset;
+ ByteSwapModelNode(data, childOffset, size);
+ }
+ }
+}
+
+static void ByteSwapModelNode(uint8_t* data, uint32_t offset, size_t size) {
+ if (!IsValidOffset(offset, size - 0x14)) return;
+ if (gVisitedNodes.count(offset)) return; // Already processed
+ gVisitedNodes.insert(offset);
+
+ uint32_t* node = reinterpret_cast<uint32_t*>(data + offset);
+
+ // Read and convert N64 addresses
+ int32_t type = static_cast<int32_t>(BSWAP32(node[0]));
+ uint32_t displayDataAddr = BSWAP32(node[1]);
+ int32_t numProperties = static_cast<int32_t>(BSWAP32(node[2]));
+ uint32_t propertyListAddr = BSWAP32(node[3]);
+ uint32_t groupDataAddr = BSWAP32(node[4]);
+
+ // Convert N64 addresses to file offsets
+ uint32_t displayData = N64AddrToOffset(displayDataAddr);
+ uint32_t propertyList = N64AddrToOffset(propertyListAddr);
+ uint32_t groupData = N64AddrToOffset(groupDataAddr);
+
+ node[0] = static_cast<uint32_t>(type);
+ node[1] = displayData;
+ node[2] = static_cast<uint32_t>(numProperties);
+ node[3] = propertyList;
+ node[4] = groupData;
+
+ // Byte-swap display data
+ if (IsValidOffset(displayData, size)) {
+ ByteSwapModelDisplayData(data, displayData, size);
+ }
+
+ // Byte-swap properties and track their offsets for vertex byte-swap exclusion
+ if (numProperties > 0 && IsValidOffset(propertyList, size)) {
+ gVisitedProperties.insert(propertyList);
+ for (int i = 0; i < numProperties; i++) {
+ ByteSwapModelNodeProperty(data, propertyList + (i * 0xC), size);
+ }
+ }
+
+ // Byte-swap group data (which recursively handles children)
+ if (IsValidOffset(groupData, size)) {
+ ByteSwapModelGroupData(data, groupData, size);
+ }
+}
+
+// Find the ROOT node (type=7) by scanning the shape data
+// Returns the file offset of the ROOT node, or 0 if not found
+static uint32_t FindRootNodeOffset(uint8_t* data, size_t size) {
+ for (uint32_t offset = 0x20; offset < size - 0x14; offset += 4) {
+ int32_t type = static_cast<int32_t>(BSWAP32(*reinterpret_cast<uint32_t*>(data + offset)));
+
+ if (type == 7) { // SHAPE_TYPE_ROOT
+ // Validate surrounding fields look like a ModelNode
+ uint32_t displayAddr = BSWAP32(*reinterpret_cast<uint32_t*>(data + offset + 0x04));
+ int32_t numProps = static_cast<int32_t>(BSWAP32(*reinterpret_cast<uint32_t*>(data + offset + 0x08)));
+ uint32_t groupAddr = BSWAP32(*reinterpret_cast<uint32_t*>(data + offset + 0x10));
+
+ bool valid = (displayAddr == 0 || displayAddr > 0x80000000);
+ valid = valid && (groupAddr == 0 || groupAddr > 0x80000000);
+ valid = valid && (numProps >= 0 && numProps <= 100);
+
+ if (valid) {
+ return offset;
+ }
+ }
+ }
+
+ SPDLOG_WARN("Could not find ROOT node in shape data");
+ return 0;
+}
+
+static void ByteSwapShapeData(uint8_t* data, size_t size, std::vector<PM64DisplayListInfo>& collectedDLs,
+ uint32_t& outVtxTableOffset, uint32_t& outVtxDataSize) {
+ outVtxTableOffset = 0;
+ outVtxDataSize = 0;
+
+ if (size < 0x20) {
+ SPDLOG_WARN("Shape data too small: {}", size);
+ return;
+ }
+
+ // Clear visited sets for this shape file
+ gVisitedNodes.clear();
+ gVisitedGroups.clear();
+ gVisitedMatrices.clear();
+ gVisitedDisplayLists.clear();
+ gVisitedDisplayData.clear();
+ gVisitedProperties.clear();
+
+ // Set up collection target
+ gCollectedDisplayLists = &collectedDLs;
+
+ // Read header values (N64 virtual addresses, big-endian)
+ uint32_t* header = reinterpret_cast<uint32_t*>(data);
+ uint32_t rootAddr = BSWAP32(header[0]);
+ uint32_t vertexTableAddr = BSWAP32(header[1]);
+ uint32_t modelNamesAddr = BSWAP32(header[2]);
+ uint32_t colliderNamesAddr = BSWAP32(header[3]);
+ uint32_t zoneNamesAddr = BSWAP32(header[4]);
+
+ // Find the ROOT node by scanning the data to compute correct base address
+ uint32_t rootFileOffset = FindRootNodeOffset(data, size);
+
+ if (rootFileOffset > 0 && rootAddr > 0x80000000) {
+ // Compute base from actual ROOT node location: base = rootAddr - rootFileOffset
+ gShapeBaseAddr = rootAddr - rootFileOffset;
+ } else {
+ // Fallback to known PM64 base (verified across all tested shapes)
+ gShapeBaseAddr = 0x80210000;
+ SPDLOG_WARN("Using fallback base address: 0x{:X}", gShapeBaseAddr);
+ }
+
+ // Convert N64 addresses to file offsets
+ uint32_t root = N64AddrToOffset(rootAddr);
+ uint32_t vertexTable = N64AddrToOffset(vertexTableAddr);
+ uint32_t modelNames = N64AddrToOffset(modelNamesAddr);
+ uint32_t colliderNames = N64AddrToOffset(colliderNamesAddr);
+ uint32_t zoneNames = N64AddrToOffset(zoneNamesAddr);
+
+ // Validate root offset is within bounds
+ if (root >= size) {
+ SPDLOG_ERROR("Root offset 0x{:X} exceeds file size {}!", root, size);
+ return;
+ }
+
+ // Set vertex table offset global BEFORE processing display lists
+ // This allows ByteSwapDisplayList to convert G_VTX offsets to vertex-table-relative
+ gVertexTableOffset = vertexTable;
+ outVtxTableOffset = vertexTable;
+
+ // Store converted offsets back to header
+ header[0] = root;
+ header[1] = vertexTable;
+ header[2] = modelNames;
+ header[3] = colliderNames;
+ header[4] = zoneNames;
+
+ // Byte-swap the root ModelNode tree recursively
+ if (IsValidOffset(root, size)) {
+ ByteSwapModelNode(data, root, size);
+ } else {
+ SPDLOG_WARN("Root offset 0x{:X} is invalid for size {}", root, size);
+ }
+
+ // Byte-swap vertex table
+ // Vtx_t structure (16 bytes):
+ // 0x00: ob[3] (3 x s16) - position
+ // 0x06: flag (u16)
+ // 0x08: tc[2] (2 x s16) - texture coords
+ // 0x0C: cn[4] (4 x u8) - color/normal (no swap needed)
+ if (IsValidOffset(vertexTable, size)) {
+ uint8_t* vtxPtr = data + vertexTable;
+ uint8_t* endPtr = data + size;
+
+ // Find the minimum offset among all visited model structures
+ // This marks where non-vertex data begins (display lists, model nodes, etc.)
+ uint32_t minVisitedOffset = size;
+ for (uint32_t off : gVisitedNodes) {
+ if (off > vertexTable && off < minVisitedOffset) minVisitedOffset = off;
+ }
+ for (uint32_t off : gVisitedGroups) {
+ if (off > vertexTable && off < minVisitedOffset) minVisitedOffset = off;
+ }
+ for (uint32_t off : gVisitedDisplayLists) {
+ if (off > vertexTable && off < minVisitedOffset) minVisitedOffset = off;
+ }
+ for (uint32_t off : gVisitedDisplayData) {
+ if (off > vertexTable && off < minVisitedOffset) minVisitedOffset = off;
+ }
+ for (uint32_t off : gVisitedProperties) {
+ if (off > vertexTable && off < minVisitedOffset) minVisitedOffset = off;
+ }
+
+ // Also check name tables and header structures
+ uint32_t vtxEnd = minVisitedOffset;
+ if (root > vertexTable && root < vtxEnd) vtxEnd = root;
+ if (modelNames > vertexTable && modelNames < vtxEnd) vtxEnd = modelNames;
+ if (colliderNames > vertexTable && colliderNames < vtxEnd) vtxEnd = colliderNames;
+ if (zoneNames > vertexTable && zoneNames < vtxEnd) vtxEnd = zoneNames;
+
+ size_t vtxSize = vtxEnd - vertexTable;
+ size_t numVertices = vtxSize / 16;
+
+ // Store vertex data size for export
+ outVtxDataSize = static_cast<uint32_t>(vtxSize);
+
+ for (size_t i = 0; i < numVertices && vtxPtr + 16 <= endPtr; i++) {
+ uint16_t* v = reinterpret_cast<uint16_t*>(vtxPtr);
+ v[0] = BSWAP16(v[0]); // ob[0]
+ v[1] = BSWAP16(v[1]); // ob[1]
+ v[2] = BSWAP16(v[2]); // ob[2]
+ v[3] = BSWAP16(v[3]); // flag
+ v[4] = BSWAP16(v[4]); // tc[0]
+ v[5] = BSWAP16(v[5]); // tc[1]
+ // cn[4] are bytes, no swap needed
+ vtxPtr += 16;
+ }
+ }
+
+ // Byte-swap name table pointers (arrays of char* terminated by "db" sentinel string)
+ // Each table is an array of BE u32 pointers to null-terminated strings.
+ // The terminator is an entry whose pointed-to string content is literally "db".
+ auto swapNameTable = [&](uint32_t tableOffset) {
+ if (!IsValidOffset(tableOffset, size - 4)) return;
+
+ uint32_t* names = reinterpret_cast<uint32_t*>(data + tableOffset);
+ while (reinterpret_cast<uint8_t*>(names) < data + size - 4) {
+ uint32_t nameAddr = BSWAP32(*names);
+ if (nameAddr == 0) {
+ *names = 0;
+ break;
+ }
+ uint32_t nameOffset = N64AddrToOffset(nameAddr);
+ // Check if the pointed-to string is "db" (the sentinel terminator)
+ if (nameOffset < size - 2) {
+ const char* str = reinterpret_cast<const char*>(data + nameOffset);
+ if (str[0] == 'd' && str[1] == 'b' && str[2] == '\0') {
+ *names = nameOffset; // still convert, runtime needs the offset
+ break;
+ }
+ }
+ *names = nameOffset;
+ names++;
+ }
+ };
+
+ swapNameTable(modelNames);
+ swapNameTable(colliderNames);
+ swapNameTable(zoneNames);
+
+ // Clear the collection pointer
+ gCollectedDisplayLists = nullptr;
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64ShapeFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+
+ std::vector<PM64DisplayListInfo> collectedDLs;
+ uint32_t vtxTableOffset = 0;
+ uint32_t vtxDataSize = 0;
+
+ // Check if compressed (YAY0)
+ auto compressionType = Decompressor::GetCompressionType(buffer, offset);
+
+ if (compressionType == CompressionType::YAY0) {
+ auto decoded = Decompressor::Decode(buffer, offset, CompressionType::YAY0);
+ if (!decoded || decoded->size == 0) {
+ SPDLOG_ERROR("Failed to decompress YAY0 shape data at offset 0x{:X}", offset);
+ return std::nullopt;
+ }
+
+ std::vector<uint8_t> shapeData(decoded->data, decoded->data + decoded->size);
+ ByteSwapShapeData(shapeData.data(), shapeData.size(), collectedDLs, vtxTableOffset, vtxDataSize);
+
+ return std::make_shared<PM64ShapeData>(std::move(shapeData), std::move(collectedDLs), vtxTableOffset, vtxDataSize);
+ } else {
+ // Uncompressed - read raw data with size from YAML
+ auto size = GetSafeNode<size_t>(node, "size");
+ auto [_, segment] = Decompressor::AutoDecode(node, buffer, size);
+
+ std::vector<uint8_t> shapeData(segment.data, segment.data + segment.size);
+ ByteSwapShapeData(shapeData.data(), shapeData.size(), collectedDLs, vtxTableOffset, vtxDataSize);
+
+ return std::make_shared<PM64ShapeData>(std::move(shapeData), std::move(collectedDLs), vtxTableOffset, vtxDataSize);
+ }
+}
+
+// Export vertex data as a separate OTR Vertex resource (V1 format with float ob[])
+// Returns the resource path used for hashing in G_VTX_OTR_HASH commands
+static std::string ExportVertexResource(const std::string& shapeName, const uint8_t* shapeData,
+ uint32_t vtxTableOffset, uint32_t vtxDataSize) {
+ if (vtxDataSize == 0) {
+ SPDLOG_WARN("No vertex data to export for shape {}", shapeName);
+ return "";
+ }
+
+ // Build resource path
+ std::string path = shapeName + "/vtx";
+ auto writer = LUS::BinaryWriter();
+
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Vertex, 0);
+
+ // Write vertex count and per-vertex data
+ // Shape data has already been byte-swapped to native endian by ByteSwapShapeData
+ uint32_t count = vtxDataSize / 16;
+ writer.Write(count);
+ for (uint32_t i = 0; i < count; i++) {
+ const uint8_t* src = shapeData + vtxTableOffset + i * 16;
+ writer.Write(*reinterpret_cast<const int16_t*>(src + 0)); // ob[0]
+ writer.Write(*reinterpret_cast<const int16_t*>(src + 2)); // ob[1]
+ writer.Write(*reinterpret_cast<const int16_t*>(src + 4)); // ob[2]
+ writer.Write(*reinterpret_cast<const uint16_t*>(src + 6)); // flag
+ writer.Write(*reinterpret_cast<const int16_t*>(src + 8)); // tc[0]
+ writer.Write(*reinterpret_cast<const int16_t*>(src + 10)); // tc[1]
+ writer.Write(src[12]); writer.Write(src[13]); writer.Write(src[14]); writer.Write(src[15]); // cn[4]
+ }
+
+ // Finish writing and register as companion file
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> data(str.begin(), str.end());
+
+ Companion::Instance->RegisterCompanionFile(path, data);
+
+ return path;
+}
+
+// Export a single display list as an OTR resource
+static void ExportDisplayListResource(const std::string& shapeName, const PM64DisplayListInfo& dlInfo) {
+ // Build the resource path
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/dlist_%X", shapeName.c_str(), dlInfo.offset);
+ std::string path = pathBuf;
+
+ // Get full OTR path for hash calculation (gCurrentDirectory + path)
+ std::string fullPath = Companion::Instance->RelativePath(path);
+
+ auto writer = LUS::BinaryWriter();
+
+ // Write DisplayList resource header
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::DisplayList, 0);
+
+ // Write GBI version byte (F3DEX2 for PM64)
+ writer.Write(static_cast<int8_t>(GBIVersion::f3dex2));
+
+ // Pad to 8-byte alignment
+ while (writer.GetBaseAddress() % 8 != 0)
+ writer.Write(static_cast<int8_t>(0xFF));
+
+ // Write G_MARKER with resource hash (using full OTR path)
+ uint64_t hash = CRC64(fullPath.c_str());
+ writer.Write(static_cast<uint32_t>(G_MARKER << 24));
+ writer.Write(static_cast<uint32_t>(0xBEEFBEEF));
+ writer.Write(static_cast<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(hash & 0xFFFFFFFF));
+
+ // Write commands in OTR format
+ // IMPORTANT: libultraship DisplayListFactory expects:
+ // - Standard commands: 8 bytes (w0, w1)
+ // - OTR-expanded commands: 16 bytes (w0, w1, extra 8 bytes)
+ // Expanded opcodes: G_SETTIMG_OTR_HASH, G_DL_OTR_HASH, G_VTX_OTR_HASH,
+ // G_BRANCH_Z_OTR, G_MARKER, G_MTX_OTR, G_MOVEMEM_OTR
+ for (size_t i = 0; i < dlInfo.commands.size(); i += 2) {
+ uint32_t w0 = dlInfo.commands[i];
+ uint32_t w1 = dlInfo.commands[i + 1];
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ if (opcode == F3DEX2_G_SETTIMG) {
+ // Replace G_SETTIMG with G_NOOP - PM64 textures are loaded via texture handle system
+ // G_NOOP is a standard 8-byte command
+ writer.Write(static_cast<uint32_t>(0x00 << 24)); // G_NOOP
+ writer.Write(static_cast<uint32_t>(0));
+ // NO PADDING - standard command is 8 bytes
+ } else if (opcode == F3DEX2_G_VTX) {
+ // Emit G_VTX_OTR_HASH - an expanded 16-byte command
+ // w0 format: opcode[31:24] | n[19:12] | (v0+n)[7:1]
+ // The n and v0+n encoding is preserved from original G_VTX
+ char vtxPath[256];
+ snprintf(vtxPath, sizeof(vtxPath), "%s/vtx", shapeName.c_str());
+ // Use RelativePath to get full OTR path (gCurrentDirectory + vtxPath)
+ std::string fullVtxPath = Companion::Instance->RelativePath(vtxPath);
+ uint64_t vtxHash = CRC64(fullVtxPath.c_str());
+
+ // Replace opcode with G_VTX_OTR_HASH, keep n and v0 encoding
+ uint32_t newW0 = (G_VTX_OTR_HASH << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(w1); // w1 is vertex-table-relative offset
+
+ // Write hash (extra 8 bytes for expanded command)
+ writer.Write(static_cast<uint32_t>(vtxHash >> 32));
+ writer.Write(static_cast<uint32_t>(vtxHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_DL) {
+ // Nested display list - build path for it and write hash
+ // G_DL_OTR_HASH is an expanded 16-byte command
+ char nestedPath[256];
+ snprintf(nestedPath, sizeof(nestedPath), "%s/dlist_%X", shapeName.c_str(), w1);
+ // Use RelativePath to get full OTR path (gCurrentDirectory + nestedPath)
+ std::string fullNestedPath = Companion::Instance->RelativePath(nestedPath);
+ uint64_t nestedHash = CRC64(fullNestedPath.c_str());
+
+ // Write G_DL_OTR_HASH opcode (expanded command - 16 bytes total)
+ N64Gfx value = gsSPDisplayListOTRHash(0);
+ writer.Write(value.words.w0);
+ writer.Write(value.words.w1);
+ // Write the hash of the nested display list (extra 8 bytes for expanded command)
+ writer.Write(static_cast<uint32_t>(nestedHash >> 32));
+ writer.Write(static_cast<uint32_t>(nestedHash & 0xFFFFFFFF));
+ } else {
+ // Standard command - 8 bytes only
+ writer.Write(w0);
+ writer.Write(w1);
+ // NO PADDING - standard commands are 8 bytes
+ }
+ }
+
+ // Finish writing and register as companion file
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> data(str.begin(), str.end());
+
+ Companion::Instance->RegisterCompanionFile(path, data);
+}
+
+ExportResult PM64ShapeBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto shapeData = std::static_pointer_cast<PM64ShapeData>(raw);
+ auto writer = LUS::BinaryWriter();
+
+ // Extract shape name from entry name (e.g., "shapes/kmr_02_shape" -> "kmr_02_shape")
+ std::string shapeName = entryName;
+ size_t lastSlash = entryName.rfind('/');
+ if (lastSlash != std::string::npos) {
+ shapeName = entryName.substr(lastSlash + 1);
+ }
+
+ // Export vertex data as a separate OTR resource
+ ExportVertexResource(shapeName, shapeData->mBuffer.data(),
+ shapeData->mVertexTableOffset, shapeData->mVertexDataSize);
+
+ // Export each display list as a separate OTR resource
+ for (const auto& dlInfo : shapeData->mDisplayLists) {
+ ExportDisplayListResource(shapeName, dlInfo);
+ }
+
+ // Write shape blob as before
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(shapeData->mBuffer.size()));
+ writer.Write(reinterpret_cast<char*>(shapeData->mBuffer.data()), shapeData->mBuffer.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64ShapeHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/ShapeFactory.h b/src/factories/pm64/ShapeFactory.h
new file mode 100644
index 0000000..70822fa
--- /dev/null
+++ b/src/factories/pm64/ShapeFactory.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+#include <vector>
+
+// Display list info collected during parsing
+struct PM64DisplayListInfo {
+ uint32_t offset; // Offset in shape file
+ std::vector<uint32_t> commands; // N64 display list commands (pairs of w0, w1)
+};
+
+// Custom parsed data that holds both shape buffer and display list info
+class PM64ShapeData : public IParsedData {
+public:
+ std::vector<uint8_t> mBuffer;
+ std::vector<PM64DisplayListInfo> mDisplayLists;
+ uint32_t mVertexTableOffset; // Offset of vertex table in shape blob
+ uint32_t mVertexDataSize; // Size of vertex data in bytes
+
+ PM64ShapeData(std::vector<uint8_t>&& buffer, std::vector<PM64DisplayListInfo>&& displayLists,
+ uint32_t vtxTableOffset, uint32_t vtxDataSize)
+ : mBuffer(std::move(buffer)), mDisplayLists(std::move(displayLists)),
+ mVertexTableOffset(vtxTableOffset), mVertexDataSize(vtxDataSize) {
+ }
+};
+
+class PM64ShapeBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64ShapeHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64ShapeFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64ShapeHeaderExporter)
+ REGISTER(Binary, PM64ShapeBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/SpriteFactory.cpp b/src/factories/pm64/SpriteFactory.cpp
new file mode 100644
index 0000000..60d2eb2
--- /dev/null
+++ b/src/factories/pm64/SpriteFactory.cpp
@@ -0,0 +1,208 @@
+#include "SpriteFactory.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include "spdlog/spdlog.h"
+#include <unordered_set>
+
+// PM64 sprite structure (decompressed, before byte-swap):
+// 0x00: rastersOffset (u32)
+// 0x04: palettesOffset (u32)
+// 0x08: maxComponents (s32)
+// 0x0C: colorVariations (s32)
+// 0x10+: animListStart[] - variable length, -1 terminated
+
+static void ByteSwapSpriteData(uint8_t* data, size_t size) {
+ if (size < 16) {
+ SPDLOG_WARN("Sprite data too small: {}", size);
+ return;
+ }
+
+ // Byte-swap the header fields (first 4 u32s)
+ uint32_t* header = reinterpret_cast<uint32_t*>(data);
+ uint32_t rastersOffset = BSWAP32(header[0]);
+ uint32_t palettesOffset = BSWAP32(header[1]);
+
+ header[0] = rastersOffset;
+ header[1] = palettesOffset;
+ header[2] = BSWAP32(header[2]); // maxComponents
+ header[3] = BSWAP32(header[3]); // colorVariations
+
+ // Byte-swap animListStart array (starts at offset 0x10)
+ // Each entry is a u32 offset, terminated by -1 (0xFFFFFFFF)
+ uint32_t* animList = reinterpret_cast<uint32_t*>(data + 0x10);
+ while (reinterpret_cast<uint8_t*>(animList) < data + size) {
+ uint32_t val = BSWAP32(*animList);
+ *animList = val;
+ if (val == 0xFFFFFFFF) {
+ break;
+ }
+ animList++;
+ }
+
+ // Byte-swap raster array entries
+ // Each raster pointer entry is a u32 offset, terminated by -1
+ if (rastersOffset > 0 && rastersOffset < size) {
+ uint32_t* rasterList = reinterpret_cast<uint32_t*>(data + rastersOffset);
+ while (reinterpret_cast<uint8_t*>(rasterList) < data + size) {
+ uint32_t val = BSWAP32(*rasterList);
+ *rasterList = val;
+ if (val == 0xFFFFFFFF) {
+ break;
+ }
+
+ // Each raster entry (SpriteRasterCacheEntry) at the pointed offset:
+ // 0x00: image offset (u32)
+ // 0x04: width (u8), height (u8), palette (s8), quadCacheIndex (s8)
+ // Only the image offset (u32) needs byte-swap
+ if (val > 0 && val < size - 4) {
+ uint32_t* rasterEntry = reinterpret_cast<uint32_t*>(data + val);
+ *rasterEntry = BSWAP32(*rasterEntry);
+ }
+
+ rasterList++;
+ }
+ }
+
+ // Byte-swap palette array entries
+ // Each palette pointer entry is a u32 offset, terminated by -1
+ if (palettesOffset > 0 && palettesOffset < size) {
+ uint32_t* paletteList = reinterpret_cast<uint32_t*>(data + palettesOffset);
+ while (reinterpret_cast<uint8_t*>(paletteList) < data + size) {
+ uint32_t val = BSWAP32(*paletteList);
+ *paletteList = val;
+ if (val == 0xFFFFFFFF) {
+ break;
+ }
+ paletteList++;
+ }
+ }
+
+ // Byte-swap animation component lists and commands
+ // Walk through each animation in animListStart
+ // IMPORTANT: PM64 sprites share data extensively - multiple animations can reference
+ // the same component list, component structure, or command list. Track processed
+ // offsets to prevent double-swapping (which would revert data to big-endian).
+ std::unordered_set<uint32_t> processedAnimLists;
+ std::unordered_set<uint32_t> processedComps;
+ std::unordered_set<uint32_t> processedCmdLists;
+
+ uint32_t* animListPtr = reinterpret_cast<uint32_t*>(data + 0x10);
+ while (reinterpret_cast<uint8_t*>(animListPtr) < data + size) {
+ uint32_t animOffset = *animListPtr;
+ if (animOffset == 0xFFFFFFFF) {
+ break;
+ }
+
+ if (animOffset > 0 && animOffset < size && !processedAnimLists.count(animOffset)) {
+ processedAnimLists.insert(animOffset);
+
+ // Each animation is a list of SpriteAnimComponent pointers, -1 terminated
+ uint32_t* compList = reinterpret_cast<uint32_t*>(data + animOffset);
+ while (reinterpret_cast<uint8_t*>(compList) < data + size) {
+ uint32_t compOffset = BSWAP32(*compList);
+ *compList = compOffset;
+ if (compOffset == 0xFFFFFFFF) {
+ break;
+ }
+
+ if (compOffset > 0 && compOffset < size - 12 && !processedComps.count(compOffset)) {
+ processedComps.insert(compOffset);
+
+ // SpriteAnimComponent structure:
+ // 0x00: cmdList offset (u32)
+ // 0x04: cmdListSize (s16)
+ // 0x06: compOffset Vec3s (3 x s16)
+ uint32_t* compData = reinterpret_cast<uint32_t*>(data + compOffset);
+ uint32_t cmdListOffset = BSWAP32(compData[0]);
+ compData[0] = cmdListOffset;
+
+ uint16_t* compData16 = reinterpret_cast<uint16_t*>(data + compOffset + 4);
+ int16_t cmdListSize = static_cast<int16_t>(BSWAP16(compData16[0]));
+ compData16[0] = cmdListSize;
+ compData16[1] = BSWAP16(compData16[1]); // compOffset.x
+ compData16[2] = BSWAP16(compData16[2]); // compOffset.y
+ compData16[3] = BSWAP16(compData16[3]); // compOffset.z
+
+ // Byte-swap command list (array of u16)
+ if (cmdListOffset > 0 && cmdListOffset < size && cmdListSize > 0 && !processedCmdLists.count(cmdListOffset)) {
+ processedCmdLists.insert(cmdListOffset);
+
+ uint16_t* cmdList = reinterpret_cast<uint16_t*>(data + cmdListOffset);
+ int numCmds = cmdListSize / 2;
+ for (int i = 0; i < numCmds && reinterpret_cast<uint8_t*>(&cmdList[i]) < data + size; i++) {
+ cmdList[i] = BSWAP16(cmdList[i]);
+ }
+ }
+ }
+ compList++;
+ }
+ }
+ animListPtr++;
+ }
+
+ // Palette pixel data (RGBA5551) is NOT byte-swapped.
+ // The Fast3D interpreter reads palette bytes as big-endian:
+ // col16 = (palette[idx*2] << 8) | palette[idx*2+1]
+ // so the raw ROM byte order must be preserved.
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64SpriteFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ // Get the offset from YAML
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+
+ // Check if this is compressed (YAY0)
+ auto compressionType = Decompressor::GetCompressionType(buffer, offset);
+
+ if (compressionType == CompressionType::YAY0) {
+ // Decompress YAY0 data
+ auto decoded = Decompressor::Decode(buffer, offset, CompressionType::YAY0);
+ if (!decoded || decoded->size == 0) {
+ SPDLOG_ERROR("Failed to decompress YAY0 sprite data at offset 0x{:X}", offset);
+ return std::nullopt;
+ }
+
+ // Create a copy of decompressed data for byte-swapping
+ std::vector<uint8_t> spriteData(decoded->data, decoded->data + decoded->size);
+
+ // Byte-swap for little-endian
+ ByteSwapSpriteData(spriteData.data(), spriteData.size());
+
+ SPDLOG_DEBUG("PM64:SPRITE parsed at 0x{:X}, decompressed size: {}", offset, spriteData.size());
+
+ return std::make_shared<RawBuffer>(spriteData);
+ } else {
+ // Uncompressed - just read raw data with size from YAML
+ auto size = GetSafeNode<size_t>(node, "size");
+ auto [_, segment] = Decompressor::AutoDecode(node, buffer, size);
+
+ std::vector<uint8_t> spriteData(segment.data, segment.data + segment.size);
+ ByteSwapSpriteData(spriteData.data(), spriteData.size());
+
+ return std::make_shared<RawBuffer>(spriteData);
+ }
+}
+
+ExportResult PM64SpriteBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ // Write as Blob type for now - game will load as raw binary
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64SpriteHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/SpriteFactory.h b/src/factories/pm64/SpriteFactory.h
new file mode 100644
index 0000000..2002cce
--- /dev/null
+++ b/src/factories/pm64/SpriteFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64SpriteBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64SpriteHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64SpriteFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64SpriteHeaderExporter)
+ REGISTER(Binary, PM64SpriteBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/StoryImageFactory.cpp b/src/factories/pm64/StoryImageFactory.cpp
new file mode 100644
index 0000000..f87c721
--- /dev/null
+++ b/src/factories/pm64/StoryImageFactory.cpp
@@ -0,0 +1,68 @@
+#include "StoryImageFactory.h"
+#include "Companion.h"
+#include "spdlog/spdlog.h"
+
+// PM64 story image factory for intro sequence graphics
+// Handles CI8 images with palettes and IA8 images without palettes
+//
+// Data layout for CI8 (has_palette=true):
+// - Image data: width * height bytes (CI8 indexed)
+// - Palette: 256 colors * 2 bytes = 512 bytes (RGBA5551)
+//
+// Data layout for IA8 (has_palette=false):
+// - Image data: width * height bytes (IA8)
+//
+// Output format: [image data][palette if CI8]
+// NOTE: Palette is kept in big-endian format because libultraship's Fast3D
+// interpreter reads palette data as big-endian (see interpreter.cpp line 749, 785-786)
+
+std::optional<std::shared_ptr<IParsedData>> PM64StoryImageFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+ auto width = GetSafeNode<uint32_t>(node, "width");
+ auto height = GetSafeNode<uint32_t>(node, "height");
+ auto hasPalette = GetSafeNode<bool>(node, "has_palette");
+
+ size_t imageSize = width * height; // CI8 or IA8 = 1 byte per pixel
+ size_t paletteSize = hasPalette ? 512 : 0; // 256 colors * 2 bytes
+ size_t totalSize = imageSize + paletteSize;
+
+ if (offset + totalSize > buffer.size()) {
+ SPDLOG_ERROR("PM64:STORY_IMAGE: Data at offset 0x{:X} exceeds buffer size (need {} bytes, have {})",
+ offset, totalSize, buffer.size() - offset);
+ return std::nullopt;
+ }
+
+ std::vector<uint8_t> result(totalSize);
+
+ // Copy image data and palette directly (no byte swapping)
+ // CI8/IA8 image data is byte-based so endianness doesn't matter
+ // Palette is kept big-endian as the interpreter expects it that way
+ std::memcpy(result.data(), buffer.data() + offset, totalSize);
+
+ return std::make_shared<RawBuffer>(result);
+}
+
+ExportResult PM64StoryImageBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ // Write as Blob type - game loads as raw binary
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64StoryImageHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/StoryImageFactory.h b/src/factories/pm64/StoryImageFactory.h
new file mode 100644
index 0000000..a75cdbc
--- /dev/null
+++ b/src/factories/pm64/StoryImageFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64StoryImageBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64StoryImageHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64StoryImageFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64StoryImageHeaderExporter)
+ REGISTER(Binary, PM64StoryImageBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/TitleDataFactory.cpp b/src/factories/pm64/TitleDataFactory.cpp
new file mode 100644
index 0000000..cb2123d
--- /dev/null
+++ b/src/factories/pm64/TitleDataFactory.cpp
@@ -0,0 +1,60 @@
+#include "TitleDataFactory.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include "spdlog/spdlog.h"
+
+// PM64 title data factory
+// Decompresses the Yay0-compressed "title_data" blob and extracts a sub-image
+// at the specified sub_offset with the specified size.
+//
+// Sub-images are byte-addressed (IA8 = 1 byte/pixel, RGBA32 = 4 bytes/pixel)
+// so no byte-swapping is needed.
+
+std::optional<std::shared_ptr<IParsedData>> PM64TitleDataFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+ auto subOffset = GetSafeNode<uint32_t>(node, "sub_offset");
+ auto size = GetSafeNode<uint32_t>(node, "size");
+
+ // Decompress Yay0
+ auto decoded = Decompressor::Decode(buffer, offset, CompressionType::YAY0);
+ if (!decoded || decoded->size == 0) {
+ SPDLOG_ERROR("PM64:TITLE_DATA: Failed to decompress YAY0 at offset 0x{:X}", offset);
+ return std::nullopt;
+ }
+
+ if (subOffset + size > decoded->size) {
+ SPDLOG_ERROR("PM64:TITLE_DATA: Sub-image at 0x{:X} + {} exceeds decompressed size {}",
+ subOffset, size, decoded->size);
+ return std::nullopt;
+ }
+
+ // Extract the sub-image (no byte-swap needed for IA8/RGBA32)
+ std::vector<uint8_t> result(size);
+ std::memcpy(result.data(), decoded->data + subOffset, size);
+
+ return std::make_shared<RawBuffer>(result);
+}
+
+ExportResult PM64TitleDataBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64TitleDataHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
diff --git a/src/factories/pm64/TitleDataFactory.h b/src/factories/pm64/TitleDataFactory.h
new file mode 100644
index 0000000..8c26fc7
--- /dev/null
+++ b/src/factories/pm64/TitleDataFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64TitleDataBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64TitleDataHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64TitleDataFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64TitleDataHeaderExporter)
+ REGISTER(Binary, PM64TitleDataBinaryExporter)
+ };
+ }
+};