diff options
| author | KiritoDv <kiritodev01@gmail.com> | 2026-03-13 12:05:57 -0600 |
|---|---|---|
| committer | KiritoDv <kiritodev01@gmail.com> | 2026-03-13 12:05:57 -0600 |
| commit | 91bfca307fc9f6f3fb77a3cb60653c69b6df526d (patch) | |
| tree | d36f106d0f8729cc38fa4b9e177f8b0a2a118154 | |
| parent | 67192cea925441501c4990104e6d3e9240092c11 (diff) | |
Implemented PM64 factories
24 files changed, 3477 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 7aab478..44a70e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ option(ENABLE_ASAN "Enable AddressSanitizer" OFF) option(BUILD_SM64 "Build with Super Mario 64 support" ON) option(BUILD_MK64 "Build with Mario Kart 64 support" ON) option(BUILD_SF64 "Build with Star Fox 64 support" ON) +option(BUILD_PM64 "Build with Paper Mario support" ON) option(BUILD_FZERO "Build with F-Zero X support" ON) option(BUILD_MARIO_ARTIST "Build with Mario Artist support" ON) option(BUILD_NAUDIO "Build with NAudio support" ON) @@ -90,6 +91,12 @@ else() list(FILTER SRC_DIR EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/src/factories/mk64/*") endif() +if(BUILD_PM64) + add_definitions(-DPM64_SUPPORT) +else() + list(FILTER SRC_DIR EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/src/factories/pm64/*") +endif() + if(BUILD_FZERO) add_definitions(-DFZERO_SUPPORT) else() 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) + }; + } +}; |
