diff --git a/src/factories/bk64/AnimFactory.cpp b/src/factories/bk64/AnimFactory.cpp index b45e3421..af3684dc 100644 --- a/src/factories/bk64/AnimFactory.cpp +++ b/src/factories/bk64/AnimFactory.cpp @@ -8,18 +8,6 @@ namespace BK64 { -ExportResult AnimHeaderExporter::Export(std::ostream& write, std::shared_ptr 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; - } - - return std::nullopt; -} - ExportResult AnimCodeExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { auto offset = GetSafeNode(node, "offset"); diff --git a/src/factories/bk64/AnimFactory.h b/src/factories/bk64/AnimFactory.h index 27484334..29ee63a7 100644 --- a/src/factories/bk64/AnimFactory.h +++ b/src/factories/bk64/AnimFactory.h @@ -1,6 +1,7 @@ #pragma once #include +#include "factories/bk64/BKHeaderExporter.h" namespace BK64 { @@ -36,11 +37,6 @@ class AnimData : public IParsedData { } }; -class AnimHeaderExporter : public BaseExporter { - ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, - YAML::Node& node, std::string* replacement) override; -}; - class AnimBinaryExporter : public BaseExporter { ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, YAML::Node& node, std::string* replacement) override; @@ -61,7 +57,7 @@ class AnimFactory : public BaseFactory { std::optional> parse(std::vector& buffer, YAML::Node& data) override; std::optional> parse_modding(std::vector& buffer, YAML::Node& data) override; inline std::unordered_map> GetExporters() override { - return { REGISTER(Code, AnimCodeExporter) REGISTER(Header, AnimHeaderExporter) + return { REGISTER(Code, AnimCodeExporter) REGISTER(Header, BKHeaderExporter) REGISTER(Binary, AnimBinaryExporter) REGISTER(Modding, AnimModdingExporter) }; } bool SupportModdedAssets() override { diff --git a/src/factories/bk64/BKAssetFactory.cpp b/src/factories/bk64/BKAssetFactory.cpp index de1a9ae5..2830d5f1 100644 --- a/src/factories/bk64/BKAssetFactory.cpp +++ b/src/factories/bk64/BKAssetFactory.cpp @@ -1,6 +1,7 @@ #include "BKAssetFactory.h" #include "Companion.h" #include "ConfigFactory.h" +#include "BKAssetTable.h" #include "spdlog/spdlog.h" #include "utils/Decompressor.h" #include "TinySHA1.hpp" @@ -14,6 +15,27 @@ namespace BK64 { +// Font masks + text-bearing models / signs / overlays a language pack +// may relocalize. +static const std::unordered_set kLangAssets = { + 0x6EB, // SPRITE_DIALOG_FONT_ALPHAMASK + 0x6EC, // SPRITE_BOLD_FONT_LETTERS_ALPHAMASK + 0x2EE, // ON_VACATIOIN_SIGN + 0x46C, // JIGSAW_PUZZLE + 0x486, // XMAS_TREE_SWITCH + 0x48B, // JIGGY_PODIUM + 0x4EA, // RACE_BANNER_FINISH + 0x4EB, // RACE_BANNER_START + 0x50A, // SHARKFOOD_ISLAND (model with sign) + 0x54C, // GAME OVER + 0x54D, // BANJO_KAZOOIE_SIGN + 0x54E, // COPYRIGHT_OVERLAY + 0x55C, // PRESS_START_OVERLAY + 0x55D, // NO_CONTROLLER_OVERLAY + 0x563, // LEVEL_ENTRY_SIGNS + 0x56C, // THE_END_SIGN +}; + static const std::unordered_map sAssetSymbolPrefixes = { { BKAssetType::Animation, "ANIM" }, { BKAssetType::Binary, "BIN" }, @@ -306,33 +328,20 @@ std::optional> BKAssetFactory::parse(std::vector sortedOffsets; - sortedOffsets.reserve(assetTableInfo.size()); + std::vector offsets; + offsets.reserve(assetTableInfo.size()); for (const auto& ai : assetTableInfo) { - sortedOffsets.push_back(ai.offset); + offsets.push_back(ai.offset); } - std::sort(sortedOffsets.begin(), sortedOffsets.end()); - sortedOffsets.erase(std::unique(sortedOffsets.begin(), sortedOffsets.end()), sortedOffsets.end()); - auto slotSize = [&](uint32_t off) -> uint32_t { - auto it = std::upper_bound(sortedOffsets.begin(), sortedOffsets.end(), off); - if (it != sortedOffsets.end()) { - return *it - off; - } - uint64_t start = static_cast(dataStartRomOffset) + off; - return start < buffer.size() ? static_cast(buffer.size() - start) : 0; - }; + const SlotSizer slotSize(offsets, dataStartRomOffset, buffer.size()); - size_t outOfOrder = 0; - for (uint32_t i = 0; i + 1 < assetCount; i++) { - if (assetTableInfo.at(i + 1).offset < assetTableInfo.at(i).offset) { - outOfOrder++; + if (slotSize.StrayCount() > 0) { + for (uint32_t i = 0; i < assetCount; i++) { + if (slotSize.IsStray(i)) { + SPDLOG_WARN(" stray table entry {} at offset 0x{:X}", i, assetTableInfo.at(i).offset); + } } } - if (outOfOrder > 0) { - SPDLOG_WARN("Asset table is not monotonic ({} out-of-order entries); sizing slots by next-higher offset", - outOfOrder); - } // Warm the decompressor cache up front, in parallel. The serial parse // pass below then mostly hits already-decoded data. @@ -393,6 +402,10 @@ std::optional> BKAssetFactory::parse(std::vector(assetOffset) + assetSize > buffer.size()) { SPDLOG_ERROR("Asset {} offset 0x{:X} + size 0x{:X} = 0x{:X} exceeds ROM " "buffer 0x{:X}", @@ -499,26 +512,6 @@ std::optional> BKAssetFactory::parse(std::vectorGetConfig().dialogPack) { const bool isText = assetType == BKAssetType::Dialog || assetType == BKAssetType::GruntyQuestion || assetType == BKAssetType::QuizQuestion; - // Font masks + text-bearing models / signs / overlays a language pack - // may relocalize. - static const std::unordered_set kLangAssets = { - 0x6EB, // SPRITE_DIALOG_FONT_ALPHAMASK (dialog/quiz/grunty text) - 0x6EC, // SPRITE_BOLD_FONT_LETTERS_ALPHAMASK (world names, headers) - 0x2EE, // ON_VACATIOIN_SIGN - 0x46C, // JIGSAW_PUZZLE - 0x486, // XMAS_TREE_SWITCH - 0x48B, // JIGGY_PODIUM - 0x4EA, // RACE_BANNER_FINISH - 0x4EB, // RACE_BANNER_START - 0x50A, // SHARKFOOD_ISLAND (model with sign) - 0x54C, // GAME OVER - 0x54D, // BANJO_KAZOOIE_SIGN - 0x54E, // COPYRIGHT_OVERLAY - 0x55C, // PRESS_START_OVERLAY - 0x55D, // NO_CONTROLLER_OVERLAY - 0x563, // LEVEL_ENTRY_SIGNS - 0x56C, // THE_END_SIGN - }; const uint32_t idx = assetInfo.index; bool isLangAsset = kLangAssets.count(idx) != 0; // The JP cart additionally carries the kana dialog font and the diff --git a/src/factories/bk64/BKAssetTable.h b/src/factories/bk64/BKAssetTable.h new file mode 100644 index 00000000..a6d580ef --- /dev/null +++ b/src/factories/bk64/BKAssetTable.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include + +namespace BK64 { + +class SlotSizer { + public: + SlotSizer(const std::vector& offsets, uint32_t dataStart, size_t romSize) + : mStray(offsets.size(), false), mDataStart(dataStart), mRomSize(romSize) { + mBoundaries.reserve(offsets.size()); + uint32_t high = 0; + for (size_t i = 0; i < offsets.size(); i++) { + if (offsets[i] < high) { + mStray[i] = true; + mStrayCount++; + continue; + } + high = offsets[i]; + mBoundaries.push_back(offsets[i]); + } + std::sort(mBoundaries.begin(), mBoundaries.end()); + mBoundaries.erase(std::unique(mBoundaries.begin(), mBoundaries.end()), mBoundaries.end()); + mRegionEnd = mBoundaries.empty() ? 0 : mBoundaries.back(); + } + + uint32_t operator()(uint32_t off) const { + if (off >= mRegionEnd) { + return 0; + } + const auto next = std::upper_bound(mBoundaries.begin(), mBoundaries.end(), off); + const uint32_t end = std::min(next != mBoundaries.end() ? *next : mRegionEnd, mRegionEnd); + if (end <= off) { + return 0; + } + const uint64_t start = static_cast(mDataStart) + off; + if (start >= mRomSize) { + return 0; + } + return static_cast(std::min(end - off, mRomSize - start)); + } + + bool IsStray(size_t index) const { + return index < mStray.size() && mStray[index]; + } + + size_t StrayCount() const { + return mStrayCount; + } + + uint32_t RegionEnd() const { + return mRegionEnd; + } + + private: + std::vector mBoundaries; + std::vector mStray; + size_t mStrayCount = 0; + uint32_t mRegionEnd = 0; + uint32_t mDataStart; + size_t mRomSize; +}; + +} // namespace BK64 diff --git a/src/factories/bk64/BKByteUtils.h b/src/factories/bk64/BKByteUtils.h new file mode 100644 index 00000000..f45ee349 --- /dev/null +++ b/src/factories/bk64/BKByteUtils.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace BK64 { + +// Big-endian scalar reads from raw ROM/overlay bytes. +inline uint16_t ReadU16BE(const uint8_t* p) { + return static_cast((p[0] << 8) | p[1]); +} + +inline int16_t ReadS16BE(const uint8_t* p) { + return static_cast(ReadU16BE(p)); +} + +inline uint32_t ReadU32BE(const uint8_t* p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +inline int32_t ReadS32BE(const uint8_t* p) { + return static_cast(ReadU32BE(p)); +} + +} // namespace BK64 diff --git a/src/factories/bk64/BKDialogShared.h b/src/factories/bk64/BKDialogShared.h new file mode 100644 index 00000000..f28e2324 --- /dev/null +++ b/src/factories/bk64/BKDialogShared.h @@ -0,0 +1,160 @@ +#pragma once + +#include "BKEmitText.h" +#include "DialogFactory.h" + +#include "spdlog/spdlog.h" +#include +#include +#include + +#include +#include +#include +#include + +namespace BK64 { + +// Both question types end their entry list with this many answer options. +constexpr int kQuestionOptionCount = 3; + +// One string's bytes as C source: printable characters quoted, control codes as hex. +inline void WriteEscapedChars(std::ostream& write, const std::string& str) { + for (auto& c : str) { + if (c < ' ') { + write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); + } else if (c == '\'') { + write << ", \'\\" << c << "\'"; + } else { + write << ", \'" << c << "\'"; + } + } +} + +// A counted run of entries as C source: the count, then `cmd, length, bytes...` per entry. +inline void WriteDialogStringArray(std::ostream& write, const std::vector& entries) { + write << fourSpaceTab << entries.size() << ",\n"; + for (const auto& [cmd, str] : entries) { + write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); + WriteEscapedChars(write, str); + write << ",\n"; + } +} + +// The same run in the binary resource: u32 count, then cmd + u32 length + raw bytes. +inline void WriteDialogStrings(LUS::BinaryWriter& writer, const std::vector& entries) { + writer.Write((uint32_t)entries.size()); + for (const auto& entry : entries) { + writer.Write(entry.cmd); + writer.Write((uint32_t)entry.str.length()); + // [port] Write(string) would prefix the length twice + writer.Write((char*)entry.str.data(), entry.str.size()); + } +} + +// The same run as a modding-yaml sequence of [cmd, "text"] pairs. +inline void EmitDialogStringSeq(YAML::Emitter& out, const std::vector& entries) { + out << YAML::BeginSeq; + for (const auto& [cmd, str] : entries) { + out << YAML::Flow; + out << YAML::BeginSeq; + out << YAML_HEX((uint32_t)cmd); + EmitText(out, str); + out << YAML::EndSeq; + } + out << YAML::EndSeq; +} + +// Does the blob still hold `count` more bytes? +inline bool HasBytes(LUS::BinaryReader& reader, size_t count) { + return reader.GetBaseAddress() + count <= reader.GetLength(); +} + +inline std::vector ReadDialogStrings(LUS::BinaryReader& reader, int count, const std::string& symbol, + const char* kind) { + std::vector entries; + for (int i = 0; i < count; i++) { + if (!HasBytes(reader, 2)) { + SPDLOG_WARN("[BK64] {} {}: entry {} of {} runs past the end of the blob; dropping the rest.", kind, symbol, + i + 1, count); + break; + } + + DialogString entry; + entry.cmd = reader.ReadUByte(); + const auto strLen = reader.ReadUByte(); + + if (!HasBytes(reader, strLen)) { + SPDLOG_WARN("[BK64] {} {}: entry {} of {} claims {} bytes the blob doesn't hold; dropping the rest.", kind, + symbol, i + 1, count, strLen); + break; + } + + entry.str = reader.ReadString(strLen); + entries.push_back(entry); + } + return entries; +} + +// Read back a [cmd, "text"] sequence from modding yaml. Strings are re-terminated +// because the game reads them as C strings. +inline std::vector ReadModdingDialogSeq(const YAML::Node& seq) { + std::vector entries; + for (YAML::const_iterator it = seq.begin(); it != seq.end(); ++it) { + DialogString entry; + entry.cmd = (*it)[0].as(); + entry.str = DecodeText((*it)[1].as()); + entry.str += '\0'; + entries.push_back(entry); + } + return entries; +} + +// Parse a modding-yaml buffer and return the asset's value node (the map under its symbol). +inline std::optional LoadModdingRoot(const std::vector& buffer) { + try { + std::string text((char*)buffer.data(), buffer.size()); + YAML::Node assetNode = YAML::Load(text.c_str()); + return assetNode.begin()->second; + } catch (YAML::ParserException& e) { + SPDLOG_ERROR("Failed to parse message data: {}", e.what()); + SPDLOG_ERROR("{}", (char*)buffer.data()); + return std::nullopt; + } +} + +// Both question types share a container. Header byte 0 is the language count, followed +// by one LE u16 start offset per language. US has a single language, so its offset table +// is the lone `05 00` and the question begins right after it; PAL carries three, with +// English first at byte 9. Either way the reader ends up on the English block, and the +// returned vector holds the start offsets of the languages after it (empty on US/JP). +// Returns nullopt (and logs) when neither header matches. +inline std::optional> SeekQuestionBlock(LUS::BinaryReader& reader, const std::string& symbol, + const char* kind, const int8_t (&usHeader)[3], + const int8_t (&palHeader)[3]) { + const auto header1 = reader.ReadInt8(); + const auto header2 = reader.ReadInt8(); + const auto header3 = reader.ReadInt8(); + + if (header1 == usHeader[0] && header2 == usHeader[1] && header3 == usHeader[2]) { + reader.ReadInt8(); // header4 (0x05) + reader.ReadInt8(); // header5 (0x00) + return std::vector{}; + } + + if (header1 == palHeader[0] && header2 == palHeader[1] && header3 == palHeader[2]) { + std::vector extraOffsets; + for (int i = 0; i < header1; i++) { + const uint16_t offset = reader.ReadUByte() | (reader.ReadUByte() << 8); + if (i > 0) { + extraOffsets.push_back(offset); // index 0 is English, where the reader now sits + } + } + return extraOffsets; + } + + SPDLOG_ERROR("Invalid Header For BK64 {} {}: {:02X} {:02X} {:02X}", kind, symbol, header1, header2, header3); + return std::nullopt; +} + +} // namespace BK64 diff --git a/src/factories/bk64/BKEmitText.h b/src/factories/bk64/BKEmitText.h index 1d48a8bb..feab5d8c 100644 --- a/src/factories/bk64/BKEmitText.h +++ b/src/factories/bk64/BKEmitText.h @@ -3,9 +3,15 @@ #include #include #include +#include #include #include +// Shared yaml/text emission formatters +#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec +#define FORMAT_HEX(x, w) \ + std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec + namespace BK64 { // Emit a text string for modding yaml. diff --git a/src/factories/bk64/BKHeaderExporter.h b/src/factories/bk64/BKHeaderExporter.h new file mode 100644 index 00000000..44d65c72 --- /dev/null +++ b/src/factories/bk64/BKHeaderExporter.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Companion.h" +#include + +namespace BK64 { + +class BKHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, + YAML::Node& node, std::string* replacement) override { + 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; + } +}; + +} // namespace BK64 diff --git a/src/factories/bk64/ConfigFactory.cpp b/src/factories/bk64/ConfigFactory.cpp index a24ee77b..a0db1761 100644 --- a/src/factories/bk64/ConfigFactory.cpp +++ b/src/factories/bk64/ConfigFactory.cpp @@ -1,4 +1,6 @@ #include "ConfigFactory.h" +#include "BKByteUtils.h" +#include "BKAssetTable.h" #include "Companion.h" #include "TinySHA1.hpp" #include "binarytools/BinaryReader.h" @@ -14,15 +16,9 @@ namespace BK64 { -// Helpers - -static uint16_t readBE16(const uint8_t* buf, uint32_t off) { - return (buf[off] << 8) | buf[off + 1]; -} +namespace fs = std::filesystem; -static uint32_t readBE32(const uint8_t* buf, uint32_t off) { - return (buf[off] << 24) | (buf[off + 1] << 16) | (buf[off + 2] << 8) | buf[off + 3]; -} +// Helpers // Simple LE binary writer struct BlobWriter { @@ -95,22 +91,185 @@ static_assert(sizeof(sBBConfigs) / sizeof(sBBConfigs[0]) == static_cast(Cod // Vanilla defaults for FCF698 data (lair overlay objects) -// D_8039347C - note door thresholds (12 doors) +// Note door thresholds (12 doors) static const uint16_t sVanillaNoteDoors[12] = { 50, 180, 260, 350, 450, 640, 765, 810, 828, 846, 864, 882 }; -// D_803947F8 - jiggy puzzles (11 entries, 4 bytes each): { u8 cost; u8 size_bits; u16 progress_flag }. +// Jiggy puzzles (11 entries, 4 bytes each): { u8 cost; u8 size_bits; u16 progress_flag }. static const uint8_t sVanillaJiggyCosts[11] = { 1, 2, 5, 7, 8, 9, 10, 12, 15, 25, 4 }; static const uint8_t sVanillaJiggySizes[11] = { 1, 2, 3, 3, 4, 4, 4, 4, 4, 5, 3 }; static const uint16_t sVanillaJiggyFlags[11] = { 0x5D, 0x5E, 0x60, 0x63, 0x66, 0x6A, 0x6E, 0x72, 0x76, 0x7A, 0x7F }; -// Vanilla level names from D_8036C58C (pause menu display) +// Vanilla level names (pause menu display) static const char* sVanillaLevelNames[13] = { "GAME TOTAL", "SPIRAL MOUNTAIN", "GRUNTILDA'S LAIR", "MUMBO'S MOUNTAIN", "TREASURE TROVE COVE", "CLANKER'S CAVERN", "BUBBLEGLOOP SWAMP", "FREEZEEZY PEAK", "GOBI'S VALLEY", "MAD MONSTER MANSION", "RUSTY BUCKET BAY", "CLICK CLOCK WOOD", "STOP 'N' SWOP" }; +// ROM layout constants (US rev0) + +// FCF698 lair-overlay data block (note doors, jiggy puzzles) +static constexpr uint32_t FCF698_ROM_OFFSET = 0x3F5CDB0; +static constexpr uint32_t FCF698_SIZE = 9888; +static constexpr uint32_t FCF698_NOTE_DOORS_OFF = 1996; +static constexpr uint32_t FCF698_JIGGY_PUZZLES_OFF = 6984; + +// Custom-code blob detection window +static constexpr uint32_t CUSTOM_CODE_ROM_OFFSET = 0x3F00000; +static constexpr uint32_t CUSTOM_CODE_RAM_BASE = 0x80400000; +static constexpr uint32_t CUSTOM_CODE_RAM_END = 0x80800000; +static constexpr uint32_t CUSTOM_CODE_RAW_MAX_SIZE = 0x100000; // raw-blob window before 0xFF trim +static constexpr int CUSTOM_CODE_MIN_INTERNAL_JALS = 4; // self-call threshold + +// Vanilla overlay locations and sizes +static constexpr uint32_t VANILLA_OVERLAY_SIZE = 902656; +static constexpr uint32_t VANILLA_F37F90_OFFSET = 0xF37F90; +static constexpr uint32_t VANILLA_CORE1_SIZE = 228336; +static constexpr uint32_t VANILLA_CORE1_ROM_OFFSET = 0xF19250; +static constexpr uint32_t VANILLA_F9CAE0 = 0xF9CAE0; + +// F9CAE0 warp-function pointer table (558 entries at 0xC3F0), and the RAM base the +// pointers are relative to. BB base: 0x80000000 + 2650000 (Form1.cs line 2538). +static constexpr uint32_t WARP_TABLE_OFF = 0xC3F0; +static constexpr int WARP_ENTRY_COUNT = 558; +static constexpr uint32_t N64_OVL_BASE = 0x80000000U + 2650000U; + +// Epilogue signature that closes a simple warp stub. +static const uint8_t kWarpEpilogue[16] = { + 0x8F, 0xBF, 0x00, 0x14, // LW $ra, 0x14($sp) + 0x27, 0xBD, 0x00, 0x18, // ADDIU $sp, $sp, 0x18 + 0x03, 0xE0, 0x00, 0x08, // JR $ra + 0x00, 0x00, 0x00, 0x00 // NOP +}; + +// Decompress an rzip blob (magic 0x11 0x72) at `off`, or nullptr. Caller frees. +static uint8_t* TryUnzipAt(const std::vector& rom, uint32_t off, uint32_t* outSize) { + *outSize = 0x100000; + if (off + 6 >= rom.size() || rom[off] != 0x11 || rom[off + 1] != 0x72) { + return nullptr; + } + try { + return bk_unzip(rom.data() + off, outSize); + } catch (...) { + return nullptr; + } +} + +static void Sha1Into(const uint8_t* data, size_t len, uint8_t out[20]) { + sha1::SHA1 s; + s.processBytes(data, len); + uint32_t digest[5]; + s.getDigest(digest); + for (int i = 0; i < 5; i++) { + out[i * 4 + 0] = (digest[i] >> 24) & 0xFF; + out[i * 4 + 1] = (digest[i] >> 16) & 0xFF; + out[i * 4 + 2] = (digest[i] >> 8) & 0xFF; + out[i * 4 + 3] = digest[i] & 0xFF; + } +} + +// 20 raw digest bytes -> 40-char lowercase hex + NUL. +static void Sha1HexString(const uint8_t sha1[20], char out[41]) { + for (int i = 0; i < 20; i++) { + std::snprintf(out + i * 2, 3, "%02x", sha1[i]); + } +} + +// Sorted unique warp-function start offsets referenced by an F9CAE0 table, so each +// scan can be bounded by the next function instead of a blind 200-byte window. +static std::vector CollectWarpFuncOffsets(const uint8_t* f9) { + std::vector funcOffs; + funcOffs.reserve(WARP_ENTRY_COUNT); + for (int w = 0; w < WARP_ENTRY_COUNT; w++) { + uint32_t addr = ReadU32BE(f9 + WARP_TABLE_OFF + w * 4); + if (addr >= N64_OVL_BASE) { + funcOffs.push_back(addr - N64_OVL_BASE); + } + } + std::sort(funcOffs.begin(), funcOffs.end()); + funcOffs.erase(std::unique(funcOffs.begin(), funcOffs.end()), funcOffs.end()); + return funcOffs; +} + +// Asset-table hashing +// +// Lighthouse ships a per-slot SHA-1 of the v1.0 baseline at assets/yaml///hashes.yaml. +// When extracting a BB romhack, we hash each slot's compressed bytes: a match means the slot is bit-for-bit +// vanilla, so skip the factory entirely. + +// Walk the BK64 asset table at ROM 0x5E90 and invoke `onSlot(index, sha1Hex)` for every non-empty, +// non-zero-sized slot. Returns the total slot count (including empty slots), or 0 on parse failure. +static size_t WalkAssetTable(const std::vector& rom, + const std::function& onSlot) { + if (rom.size() < 0x6000) { + SPDLOG_ERROR("ROM too small to contain BK64 asset table"); + return 0; + } + + constexpr uint32_t kAssetTableOffset = 0x5E90; + + LUS::BinaryReader reader(reinterpret_cast(const_cast(rom.data() + kAssetTableOffset)), + rom.size() - kAssetTableOffset); + reader.SetEndianness(Torch::Endianness::Big); + const uint32_t assetCount = reader.ReadUInt32(); + reader.ReadUInt32(); + + if (assetCount == 0 || assetCount > 0x10000) { + SPDLOG_ERROR("Implausible asset count {} at 0x{:X}", assetCount, kAssetTableOffset); + return 0; + } + + const uint32_t dataStart = kAssetTableOffset + 8 + assetCount * 8; + if (dataStart >= rom.size()) { + SPDLOG_ERROR("Asset table extends past ROM end"); + return 0; + } + + struct Entry { + uint32_t blobOffset; + int16_t compressionFlag; + int16_t tFlag; + }; + std::vector entries(assetCount); + for (uint32_t i = 0; i < assetCount; i++) { + entries[i].blobOffset = reader.ReadUInt32(); + entries[i].compressionFlag = reader.ReadInt16(); + entries[i].tFlag = reader.ReadInt16(); + } + + std::vector offsets; + offsets.reserve(assetCount); + for (const auto& e : entries) { + offsets.push_back(e.blobOffset); + } + const SlotSizer slotSize(offsets, dataStart, rom.size()); + + for (uint32_t i = 0; i < assetCount; i++) { + const auto& e = entries[i]; + if (e.tFlag == 4 || slotSize.IsStray(i)) { + continue; + } + + const uint32_t absOff = dataStart + e.blobOffset; + const uint32_t size = slotSize(e.blobOffset); + if (size == 0 || absOff + size > rom.size()) { + continue; + } + + sha1::SHA1 s; + s.processBytes(rom.data() + absOff, size); + uint32_t digest[5]; + s.getDigest(digest); + char buf[41]; + std::snprintf(buf, sizeof(buf), "%08x%08x%08x%08x%08x", digest[0], digest[1], digest[2], digest[3], digest[4]); + + onSlot(i, std::string(buf)); + } + + return assetCount; +} + // Find patched overlay via EOR boot code pointers static uint8_t* FindPatchedOverlay(const std::vector& rom, uint32_t& outSize, uint32_t& outRomAddr) { @@ -187,19 +346,10 @@ static uint8_t* FindRelocatedF9CAE0(const std::vector& rom, uint32_t f3 return nullptr; } -static constexpr uint32_t FCF698_ROM_OFFSET = 0x3F5CDB0; -static constexpr uint32_t FCF698_SIZE = 9888; -static constexpr uint32_t FCF698_NOTE_DOORS_OFF = 1996; -static constexpr uint32_t FCF698_JIGGY_PUZZLES_OFF = 6984; - // No-vanilla variant: find the warp stub's destination immediate in a single // overlay. Used when the hack overwrote its stock F9CAE0/F37F90 copies, so there // is nothing to diff against and every resolvable destination is emitted. static bool ScanWarpDestOnly(const uint8_t* ovl, uint32_t funcOff, uint32_t funcLen, uint32_t ovlSize, int& outDest) { - static const uint8_t kEpilogue[16] = { - 0x8F, 0xBF, 0x00, 0x14, 0x27, 0xBD, 0x00, 0x18, - 0x03, 0xE0, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00 - }; outDest = -1; uint32_t limit = std::min(funcLen, 200); if (funcOff + limit > ovlSize || limit < 20) { @@ -207,7 +357,7 @@ static bool ScanWarpDestOnly(const uint8_t* ovl, uint32_t funcOff, uint32_t func } const uint8_t* f = ovl + funcOff; for (uint32_t i = 0; i + 20 <= limit; i += 4) { - if ((f[i] == 0x24 || f[i] == 0x34) && f[i + 1] == 0x05 && memcmp(f + i + 4, kEpilogue, 16) == 0) { + if ((f[i] == 0x24 || f[i] == 0x34) && f[i + 1] == 0x05 && memcmp(f + i + 4, kWarpEpilogue, 16) == 0) { outDest = (f[i + 2] << 8) | f[i + 3]; return true; } @@ -220,13 +370,6 @@ static bool ScanWarpDestOnly(const uint8_t* ovl, uint32_t funcOff, uint32_t func static bool ScanWarpDestDiff(const uint8_t* vanOvl, const uint8_t* modOvl, uint32_t funcOff, uint32_t funcLen, uint32_t ovlSize, int& outVanDest, int& outModDest) { - static const uint8_t kEpilogue[16] = { - 0x8F, 0xBF, 0x00, 0x14, // LW $ra, 0x14($sp) - 0x27, 0xBD, 0x00, 0x18, // ADDIU $sp, $sp, 0x18 - 0x03, 0xE0, 0x00, 0x08, // JR $ra - 0x00, 0x00, 0x00, 0x00 // NOP - }; - outVanDest = -1; outModDest = -1; uint32_t limit = std::min(funcLen, 200); @@ -238,7 +381,7 @@ static bool ScanWarpDestDiff(const uint8_t* vanOvl, const uint8_t* modOvl, uint3 const uint8_t* mod = modOvl + funcOff; for (uint32_t i = 0; i + 20 <= limit; i += 4) { if ((van[i] == 0x24 || van[i] == 0x34) && van[i + 1] == 0x05 && - memcmp(van + i + 4, kEpilogue, 16) == 0) { + memcmp(van + i + 4, kWarpEpilogue, 16) == 0) { // Same load must still be present in the modified overlay. if ((mod[i] == 0x24 || mod[i] == 0x34) && mod[i + 1] == 0x05) { int vanDest = (van[i + 2] << 8) | van[i + 3]; @@ -278,16 +421,6 @@ static bool ScanWarpDestDiff(const uint8_t* vanOvl, const uint8_t* modOvl, uint3 // The blob is stored either rzip-compressed (header `0x11 0x72`) somewhere in high ROM, // or as raw uncompressed MIPS at exactly 0x3F00000 that the boot stub DMA-copies verbatim. -static constexpr uint32_t CUSTOM_CODE_ROM_OFFSET = 0x3F00000; -static constexpr uint32_t CUSTOM_CODE_RAM_BASE = 0x80400000; -static constexpr uint32_t CUSTOM_CODE_RAM_END = 0x80800000; -static constexpr uint32_t CUSTOM_CODE_RAW_MAX_SIZE = 0x100000; // raw-blob window before 0xFF trim -static constexpr int CUSTOM_CODE_MIN_INTERNAL_JALS = 4; // self-call threshold -static constexpr uint32_t VANILLA_OVERLAY_SIZE = 902656; -static constexpr uint32_t VANILLA_F37F90_OFFSET = 0xF37F90; -static constexpr uint32_t VANILLA_CORE1_SIZE = 228336; -static constexpr uint32_t VANILLA_CORE1_ROM_OFFSET = 0xF19250; - static bool LooksLikeMipsFunctionPrologue(const uint8_t* p) { // addiu $sp, $sp, -N -> 0x27 0xBD 0xFF 0xxx return p[0] == 0x27 && p[1] == 0xBD && p[2] == 0xFF; @@ -348,37 +481,15 @@ static bool CheckCustomCodePayload(const std::vector& payload, int& out outFirstInternalTarget = firstTarget; outRamBase = firstTarget & ~0xFFFFFu; if (outBlobSha1) { - sha1::SHA1 s; - s.processBytes(payload.data(), payload.size()); - uint32_t digest[5]; - s.getDigest(digest); - for (int i = 0; i < 5; i++) { - outBlobSha1[i * 4 + 0] = (digest[i] >> 24) & 0xFF; - outBlobSha1[i * 4 + 1] = (digest[i] >> 16) & 0xFF; - outBlobSha1[i * 4 + 2] = (digest[i] >> 8) & 0xFF; - outBlobSha1[i * 4 + 3] = digest[i] & 0xFF; - } + Sha1Into(payload.data(), payload.size(), outBlobSha1); } return true; } -static void Sha1Into(const uint8_t* data, size_t len, uint8_t out[20]) { - sha1::SHA1 s; - s.processBytes(data, len); - uint32_t digest[5]; - s.getDigest(digest); - for (int i = 0; i < 5; i++) { - out[i * 4 + 0] = (digest[i] >> 24) & 0xFF; - out[i * 4 + 1] = (digest[i] >> 16) & 0xFF; - out[i * 4 + 2] = (digest[i] >> 8) & 0xFF; - out[i * 4 + 3] = digest[i] & 0xFF; - } -} - // Collect j/jal targets landing in the custom-code RAM window from a decompressed overlay. static void CollectExtendedRamJumps(const uint8_t* ovl, uint32_t size, std::vector& outTargets) { for (uint32_t off = 0; off + 4 <= size; off += 4) { - uint32_t w = readBE32(ovl, off); + uint32_t w = ReadU32BE(ovl + off); uint32_t op = w >> 26; if (op != 2 && op != 3) { continue; @@ -417,7 +528,7 @@ static bool DetectOverlayHookedCustomCode(const std::vector& rom, uint3 if (rom[off] != 0x11 || rom[off + 1] != 0x72) { continue; } - if (readBE32(rom.data(), off + 2) == VANILLA_CORE1_SIZE) { + if (ReadU32BE(rom.data() + off + 2) == VANILLA_CORE1_SIZE) { c1Addr = off; // keep the last hit (closest to core2) } } @@ -451,7 +562,7 @@ static bool DetectOverlayHookedCustomCode(const std::vector& rom, uint3 // Cluster them; asset data yields sparse coincidental hits, the blob a dense run. std::vector hits; for (uint32_t off = 0; off + 4 <= rom.size(); off += 4) { - uint32_t w = readBE32(rom.data(), off); + uint32_t w = ReadU32BE(rom.data() + off); uint32_t op = w >> 26; if (op != 2 && op != 3) { continue; @@ -511,7 +622,7 @@ static bool DetectOverlayHookedCustomCode(const std::vector& rom, uint3 int live = 0; for (uint32_t t : targets) { uint32_t off = start + (t - ramBase); - if (off + 4 <= end && readBE32(rom.data(), off) != 0) { + if (off + 4 <= end && ReadU32BE(rom.data() + off) != 0) { live++; } } @@ -684,9 +795,7 @@ bool GetCustomCodeBlobInfo(const std::vector& rom, CustomCodeKind& outK if (!DetectCustomCodeBlob(rom, blobRom, jalCount, firstTarget, ramBase, outKind, sha1)) { return false; } - for (int i = 0; i < 20; i++) { - std::snprintf(outSha1Hex + i * 2, 3, "%02x", sha1[i]); - } + Sha1HexString(sha1, outSha1Hex); return true; } @@ -719,9 +828,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std customCodeRamBase, customCodeKind, customCodeSha1); if (hasCustomCode) { char hex[41]; - for (int i = 0; i < 20; i++) { - std::snprintf(hex + i * 2, 3, "%02x", customCodeSha1[i]); - } + Sha1HexString(customCodeSha1, hex); if (customCodeKind == CustomCodeKind::BB_INJECTED) { SPDLOG_WARN("[ConfigFactory] Injected custom MIPS code detected at ROM 0x{:X}; {} internal jal " "self-references seen, first target 0x{:08X}; sha1={}.", @@ -820,7 +927,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std if (entry.offset + 2 > overlaySize) continue; - uint16_t val = entry.isByte ? overlay[entry.offset] : readBE16(overlay, entry.offset); + uint16_t val = entry.isByte ? overlay[entry.offset] : ReadU16BE(overlay + entry.offset); if (val != entry.vanilla) { blob.writeU16(static_cast(i)); blob.writeU16(val); @@ -843,13 +950,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std uint8_t* modF9 = FindRelocatedF9CAE0(rom, f37RomAddr, modSize); // Vanilla F9CAE0 at original location - uint8_t* vanF9 = nullptr; - static constexpr uint32_t VANILLA_F9CAE0 = 0xF9CAE0; - if (VANILLA_F9CAE0 + 6 < rom.size() && rom[VANILLA_F9CAE0] == 0x11 && rom[VANILLA_F9CAE0 + 1] == 0x72) { - try { - vanF9 = bk_unzip(rom.data() + VANILLA_F9CAE0, &vanSize); - } catch (...) {} - } + uint8_t* vanF9 = TryUnzipAt(rom, VANILLA_F9CAE0, &vanSize); // Some hacks relocate F9CAE0 and overwrite the stock copy, // so there is no vanilla reference in the ROM to diff against. @@ -868,14 +969,14 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std uint16_t count = 0; for (uint32_t off = 0x8288; off + 4 <= f9Size; off += 8) { - uint16_t mapId = readBE16(modF9, off); + uint16_t mapId = ReadU16BE(modF9 + off); if (mapId == 0 && off > 0x8288 + 8) break; if (mapId == 0) continue; - uint16_t modLevel = readBE16(modF9, off + 2); - uint16_t vanLevel = haveVanF9 ? readBE16(vanF9, off + 2) : 0; + uint16_t modLevel = ReadU16BE(modF9 + off + 2); + uint16_t vanLevel = haveVanF9 ? ReadU16BE(vanF9 + off + 2) : 0; if (!haveVanF9 || modLevel != vanLevel) { blob.writeU16(mapId); blob.writeU16(modLevel); @@ -900,10 +1001,10 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std for (int i = 0; i < 11; i++) { uint32_t off = 0x8FD0 + i * 4; - uint16_t modMap = readBE16(modF9, off); - uint16_t modExit = readBE16(modF9, off + 2); - uint16_t vanMap = haveVanF9 ? readBE16(vanF9, off) : 0; - uint16_t vanExit = haveVanF9 ? readBE16(vanF9, off + 2) : 0; + uint16_t modMap = ReadU16BE(modF9 + off); + uint16_t modExit = ReadU16BE(modF9 + off + 2); + uint16_t vanMap = haveVanF9 ? ReadU16BE(vanF9 + off) : 0; + uint16_t vanExit = haveVanF9 ? ReadU16BE(vanF9 + off + 2) : 0; if (!haveVanF9 || modMap != vanMap || modExit != vanExit) { blob.writeU8(static_cast(i)); blob.writeU8(0); // pad @@ -930,7 +1031,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std uint16_t count = 0; for (uint32_t off = 0xA8F0; off + 8 <= f9Size; off += 8) { - uint16_t mapId = readBE16(modF9, off); + uint16_t mapId = ReadU16BE(modF9 + off); if (mapId == 0 && off > 0xA8F0 + 8) break; if (mapId == 0) @@ -939,8 +1040,8 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std bool differs = !haveVanF9 || memcmp(modF9 + off, vanF9 + off, 8) != 0; if (differs) { blob.writeU16(mapId); - blob.writeU16(readBE16(modF9, off + 2)); - blob.writeU16(readBE16(modF9, off + 4)); + blob.writeU16(ReadU16BE(modF9 + off + 2)); + blob.writeU16(ReadU16BE(modF9 + off + 4)); count++; } } @@ -967,7 +1068,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std std::vector hackMaps; for (uint32_t off = 0x87B0; off + 40 <= f9Size; off += 40) { - uint16_t sceneId = readBE16(modF9, off); + uint16_t sceneId = ReadU16BE(modF9 + off); if (sceneId == 0 && off > 0x87B0 + 40) break; if (sceneId == 0) @@ -981,13 +1082,13 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std // u32 LE) for (int layer = 0; layer < 3; layer++) { uint32_t base = off + 4 + layer * 12; - blob.writeS16(static_cast(readBE16(modF9, base))); // model_id - blob.writeU32(readBE32(modF9, base + 4)); // scale (BE f32 bits -> LE u32) - blob.writeU32(readBE32(modF9, base + 8)); // rotation (BE f32 bits -> LE u32) + blob.writeS16(static_cast(ReadU16BE(modF9 + base))); // model_id + blob.writeU32(ReadU32BE(modF9 + base + 4)); // scale (BE f32 bits -> LE u32) + blob.writeU32(ReadU32BE(modF9 + base + 8)); // rotation (BE f32 bits -> LE u32) } count++; SPDLOG_INFO("[ConfigFactory] skybox: scene 0x{:X} models [{},{},{}]", sceneId, - readBE16(modF9, off + 4), readBE16(modF9, off + 16), readBE16(modF9, off + 28)); + ReadU16BE(modF9 + off + 4), ReadU16BE(modF9 + off + 16), ReadU16BE(modF9 + off + 28)); } } @@ -996,7 +1097,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std // to its built-in vanilla table and drawing a sky the hack deleted. // An all-zero entry makes the runtime override resolve to "no skybox". for (uint32_t off = 0x87B0; haveVanF9 && off + 40 <= f9Size; off += 40) { - uint16_t vanId = readBE16(vanF9, off); + uint16_t vanId = ReadU16BE(vanF9 + off); if (vanId == 0 && off > 0x87B0 + 40) break; if (vanId == 0) @@ -1029,7 +1130,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std uint16_t count = 0; for (uint32_t off = 0x7650; off + 24 <= f9Size; off += 24) { - uint16_t sceneId = readBE16(modF9, off); + uint16_t sceneId = ReadU16BE(modF9 + off); if (sceneId == 0 && off > 0x7650 + 24) break; if (sceneId == 0) @@ -1038,15 +1139,15 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std bool differs = !haveVanF9 || memcmp(modF9 + off, vanF9 + off, 24) != 0; if (differs) { blob.writeU16(sceneId); - blob.writeS16(static_cast(readBE16(modF9, off + 2))); // opa - blob.writeS16(static_cast(readBE16(modF9, off + 4))); // xlu + blob.writeS16(static_cast(ReadU16BE(modF9 + off + 2))); // opa + blob.writeS16(static_cast(ReadU16BE(modF9 + off + 4))); // xlu for (int b = 0; b < 3; b++) { - blob.writeS16(static_cast(readBE16(modF9, off + 6 + b * 2))); // min[3] + blob.writeS16(static_cast(ReadU16BE(modF9 + off + 6 + b * 2))); // min[3] } for (int b = 0; b < 3; b++) { - blob.writeS16(static_cast(readBE16(modF9, off + 12 + b * 2))); // max[3] + blob.writeS16(static_cast(ReadU16BE(modF9 + off + 12 + b * 2))); // max[3] } - blob.writeU32(readBE32(modF9, off + 20)); // scale (BE f32 bits -> LE u32) + blob.writeU32(ReadU32BE(modF9 + off + 20)); // scale (BE f32 bits -> LE u32) count++; } } @@ -1108,19 +1209,8 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std // original ROM location, scan both copies for the ADDIU $a1 dest // pattern, and emit diffs. { - static constexpr uint32_t WARP_TABLE_OFF = 0xC3F0; // 50160 decimal - static constexpr int WARP_ENTRY_COUNT = 558; // (52392 - 50160) / 4 - // BB base: 0x80000000 + 2650000 (Form1.cs line 2538) - static constexpr uint32_t N64_OVL_BASE = 0x80000000U + 2650000U; - - uint8_t* vanOvl = nullptr; - uint32_t vanOvlSize = 0x100000; - if (VANILLA_F37F90_OFFSET + 6 < rom.size() && rom[VANILLA_F37F90_OFFSET] == 0x11 && - rom[VANILLA_F37F90_OFFSET + 1] == 0x72) { - try { - vanOvl = bk_unzip(rom.data() + VANILLA_F37F90_OFFSET, &vanOvlSize); - } catch (...) {} - } + uint32_t vanOvlSize = 0; + uint8_t* vanOvl = TryUnzipAt(rom, VANILLA_F37F90_OFFSET, &vanOvlSize); // With a vanilla reference we emit only warps whose destination changed. // Without one, the hack's own F9CAE0 still holds a valid warp function @@ -1131,23 +1221,12 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std size_t secPos = blob.beginSection(ConfigSectionType::WARP_DESTINATIONS); uint16_t count = 0; - // Sorted unique function starts, so each scan can be bounded by the - // next table-referenced function instead of a blind 200-byte window. - std::vector funcOffs; - funcOffs.reserve(WARP_ENTRY_COUNT); - for (int w = 0; w < WARP_ENTRY_COUNT; w++) { - uint32_t addr = readBE32(warpF9, WARP_TABLE_OFF + w * 4); - if (addr >= N64_OVL_BASE) { - funcOffs.push_back(addr - N64_OVL_BASE); - } - } - std::sort(funcOffs.begin(), funcOffs.end()); - funcOffs.erase(std::unique(funcOffs.begin(), funcOffs.end()), funcOffs.end()); + const std::vector funcOffs = CollectWarpFuncOffsets(warpF9); uint32_t scanSize = warpDiffMode ? std::min(vanOvlSize, overlaySize) : overlaySize; for (int w = 0; w < WARP_ENTRY_COUNT; w++) { uint32_t tOff = WARP_TABLE_OFF + w * 4; - uint32_t addr = readBE32(warpF9, tOff); + uint32_t addr = ReadU32BE(warpF9 + tOff); if (addr < N64_OVL_BASE) { continue; } @@ -1157,10 +1236,15 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std uint32_t funcLen = (next != funcOffs.end()) ? (*next - funcOff) : 200; int vanDest = -1, modDest = -1; - const bool emit = warpDiffMode - ? ScanWarpDestDiff(vanOvl, overlay, funcOff, funcLen, scanSize, vanDest, - modDest) - : ScanWarpDestOnly(overlay, funcOff, funcLen, scanSize, modDest); + bool emit; + if (warpDiffMode) { + emit = ScanWarpDestDiff(vanOvl, overlay, funcOff, funcLen, scanSize, vanDest, modDest); + } else { + // No in-ROM vanilla table to diff against (relocated F9CAE0): + // compare against the vanilla destinations from hashes.yaml. + vanDest = GetVanillaWarpDest(static_cast(w)); + emit = ScanWarpDestOnly(overlay, funcOff, funcLen, scanSize, modDest) && modDest != vanDest; + } if (emit) { blob.writeU16(static_cast(w)); blob.writeU16(static_cast(modDest)); @@ -1206,7 +1290,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std } // Validate: first note door value should be a reasonable u16 (0-10000) - uint16_t firstDoor = readBE16(fcf, FCF698_NOTE_DOORS_OFF); + uint16_t firstDoor = ReadU16BE(fcf + FCF698_NOTE_DOORS_OFF); if (regionPresent && firstDoor <= 10000) { // Section 7: NOTE_DOORS { @@ -1214,7 +1298,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std uint16_t count = 0; for (int i = 0; i < 12; i++) { - uint16_t val = readBE16(fcf, FCF698_NOTE_DOORS_OFF + i * 2); + uint16_t val = ReadU16BE(fcf + FCF698_NOTE_DOORS_OFF + i * 2); if (val != sVanillaNoteDoors[i]) { blob.writeU8(static_cast(i)); blob.writeU8(0); // pad @@ -1240,7 +1324,7 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std for (int i = 0; i < 11; i++) { uint8_t cost = fcf[FCF698_JIGGY_PUZZLES_OFF + i * 4]; uint8_t size = fcf[FCF698_JIGGY_PUZZLES_OFF + i * 4 + 1]; - uint16_t flag = readBE16(fcf, FCF698_JIGGY_PUZZLES_OFF + i * 4 + 2); + uint16_t flag = ReadU16BE(fcf + FCF698_JIGGY_PUZZLES_OFF + i * 4 + 2); if (cost != sVanillaJiggyCosts[i] || size != sVanillaJiggySizes[i] || flag != sVanillaJiggyFlags[i]) { blob.writeU8(static_cast(i)); @@ -1281,92 +1365,6 @@ std::vector BuildGameConfigBlob(const std::vector& rom, const std return blob.data; } -// Asset-table hashing -// -// Lighthouse ships a per-slot SHA-1 of the v1.0 baseline at assets/yaml///hashes.yaml. -// When extracting a BB romhack, we hash each slot's compressed bytes: a match means the slot is bit-for-bit -// vanilla, so skip the factory entirely. - -// Walk the BK64 asset table at ROM 0x5E90 and invoke `onSlot(index, sha1Hex)` for every non-empty, -// non-zero-sized slot. Returns the total slot count (including empty slots), or 0 on parse failure. -static size_t WalkAssetTable(const std::vector& rom, - const std::function& onSlot) { - if (rom.size() < 0x6000) { - SPDLOG_ERROR("ROM too small to contain BK64 asset table"); - return 0; - } - - constexpr uint32_t kAssetTableOffset = 0x5E90; - - LUS::BinaryReader reader(reinterpret_cast(const_cast(rom.data() + kAssetTableOffset)), - rom.size() - kAssetTableOffset); - reader.SetEndianness(Torch::Endianness::Big); - const uint32_t assetCount = reader.ReadUInt32(); - reader.ReadUInt32(); - - if (assetCount == 0 || assetCount > 0x10000) { - SPDLOG_ERROR("Implausible asset count {} at 0x{:X}", assetCount, kAssetTableOffset); - return 0; - } - - const uint32_t dataStart = kAssetTableOffset + 8 + assetCount * 8; - if (dataStart >= rom.size()) { - SPDLOG_ERROR("Asset table extends past ROM end"); - return 0; - } - - struct Entry { - uint32_t blobOffset; - int16_t compressionFlag; - int16_t tFlag; - }; - std::vector entries(assetCount); - for (uint32_t i = 0; i < assetCount; i++) { - entries[i].blobOffset = reader.ReadUInt32(); - entries[i].compressionFlag = reader.ReadInt16(); - entries[i].tFlag = reader.ReadInt16(); - } - - for (uint32_t i = 0; i < assetCount; i++) { - const auto& e = entries[i]; - if (e.tFlag == 4) { - continue; - } - - uint32_t endRel = 0; - bool haveEnd = false; - for (uint32_t j = i + 1; j < assetCount; j++) { - if (entries[j].blobOffset != e.blobOffset) { - endRel = entries[j].blobOffset; - haveEnd = true; - break; - } - } - if (!haveEnd) { - endRel = static_cast(rom.size() - dataStart); - } - if (endRel <= e.blobOffset) { - continue; - } - const uint32_t absOff = dataStart + e.blobOffset; - const uint32_t size = endRel - e.blobOffset; - if (absOff + size > rom.size()) { - continue; - } - - sha1::SHA1 s; - s.processBytes(rom.data() + absOff, size); - uint32_t digest[5]; - s.getDigest(digest); - char buf[41]; - std::snprintf(buf, sizeof(buf), "%08x%08x%08x%08x%08x", digest[0], digest[1], digest[2], digest[3], digest[4]); - - onSlot(i, std::string(buf)); - } - - return assetCount; -} - void EmitAssetHashes(const std::filesystem::path& romPath, const std::filesystem::path& outputPath) { std::ifstream input(romPath, std::ios::binary); if (!input) { @@ -1392,14 +1390,51 @@ void EmitAssetHashes(const std::filesystem::path& romPath, const std::filesystem out << " 0x" << std::hex << std::uppercase << i << std::dec << ": " << sha1 << "\n"; hashed++; }); + + // Vanilla warp destinations, consumed by the WARP_DESTINATIONS emitter when a + // hack relocates F9CAE0 and no in-ROM vanilla table is left to diff against. + uint32_t f9Size = 0; + uint8_t* f9 = TryUnzipAt(rom, VANILLA_F9CAE0, &f9Size); + uint32_t ovlSize = 0; + uint8_t* ovl = TryUnzipAt(rom, VANILLA_F37F90_OFFSET, &ovlSize); + + size_t warpCount = 0; + if (f9 && ovl && f9Size > WARP_TABLE_OFF + WARP_ENTRY_COUNT * 4) { + out << "\n# Vanilla warp destinations (MAP<<8 | ENTRY) by warp-table index, scanned\n" + << "# from the F9CAE0 table at 0xC3F0. Warp functions with no single scannable\n" + << "# destination are omitted.\n" + << "warps:\n"; + + const std::vector funcOffs = CollectWarpFuncOffsets(f9); + + for (int w = 0; w < WARP_ENTRY_COUNT; w++) { + uint32_t addr = ReadU32BE(f9 + WARP_TABLE_OFF + w * 4); + if (addr < N64_OVL_BASE) { + continue; + } + uint32_t funcOff = addr - N64_OVL_BASE; + auto next = std::upper_bound(funcOffs.begin(), funcOffs.end(), funcOff); + uint32_t funcLen = (next != funcOffs.end()) ? (*next - funcOff) : 200; + int dest = -1; + if (ScanWarpDestOnly(ovl, funcOff, funcLen, ovlSize, dest)) { + out << " " << w << ": 0x" << std::hex << std::uppercase << dest << std::dec << "\n"; + warpCount++; + } + } + } + if (f9) { + free(f9); + } + if (ovl) { + free(ovl); + } out.close(); - SPDLOG_INFO("Hashed {} of {} slots ({} empty/zero-sized) -> {}", hashed, total, total - hashed, - outputPath.string()); + SPDLOG_INFO("Hashed {} of {} slots ({} empty/zero-sized), {} vanilla warps -> {}", hashed, total, total - hashed, + warpCount, outputPath.string()); } size_t CountModifiedSlots(const std::vector& rom, const std::filesystem::path& hashesYamlPath) { - namespace fs = std::filesystem; if (!fs::exists(hashesYamlPath)) { SPDLOG_WARN("hashes.yaml not found at {}; cannot count modified slots", hashesYamlPath.string()); return 0; @@ -1440,7 +1475,6 @@ BaselineHashCache& GetBaselineCache() { } // namespace const std::string& GetBaselineAssetHash(uint32_t assetId) { - namespace fs = std::filesystem; static const std::string kEmpty; auto& cache = GetBaselineCache(); @@ -1467,6 +1501,43 @@ const std::string& GetBaselineAssetHash(uint32_t assetId) { return (it != cache.entries.end()) ? it->second : kEmpty; } +namespace { +struct VanillaWarpCache { + std::unordered_map entries; + bool attempted = false; +}; +VanillaWarpCache& GetVanillaWarpCache() { + static VanillaWarpCache cache; + return cache; +} +} // namespace + +int GetVanillaWarpDest(uint32_t warpIndex) { + + auto& cache = GetVanillaWarpCache(); + if (!cache.attempted) { + cache.attempted = true; + const auto path = fs::path(Companion::Instance->GetAssetPath()) / "hashes.yaml"; + if (fs::exists(path)) { + try { + YAML::Node root = YAML::LoadFile(path.string()); + YAML::Node warps = root["warps"]; + if (warps && warps.IsMap()) { + for (auto it = warps.begin(); it != warps.end(); ++it) { + cache.entries[it->first.as()] = it->second.as(); + } + SPDLOG_INFO("Loaded {} vanilla warp destinations from {}", cache.entries.size(), path.string()); + } + } catch (const std::exception& e) { + SPDLOG_WARN("Failed to load vanilla warps at {}: {}", path.string(), e.what()); + } + } + } + + auto it = cache.entries.find(warpIndex); + return (it != cache.entries.end()) ? it->second : -1; +} + bool TrySynthesizeRomConfig(YAML::Node& config, const std::string& cartHash, const std::filesystem::path& romPath, const std::vector& rom) { if (config[cartHash]) { diff --git a/src/factories/bk64/ConfigFactory.h b/src/factories/bk64/ConfigFactory.h index 4a2a6f66..b7b7cce2 100644 --- a/src/factories/bk64/ConfigFactory.h +++ b/src/factories/bk64/ConfigFactory.h @@ -8,27 +8,6 @@ namespace BK64 { -// Vanilla BK is exactly 16MB. Anything bigger has been through Banjo's Backpack. -inline bool IsRomhack(const std::vector& rom) { - return rom.size() > 0x1000000; -} - -// When a romhack, generate a config for it based on us v1.0. -bool TrySynthesizeRomConfig(YAML::Node& config, const std::string& cartHash, const std::filesystem::path& romPath, - const std::vector& rom); - -// Maintainer tool, creates a hashes.yml. -void EmitAssetHashes(const std::filesystem::path& romPath, const std::filesystem::path& outputPath); - -// How many slots differ from the baseline hashes - a missing baseline entry counts as -// changed. The port-side extractor uses it to size its progress bar to the real romhack -// delta instead of every vanilla slot. -size_t CountModifiedSlots(const std::vector& rom, const std::filesystem::path& hashesYamlPath); - -// Baseline SHA-1 for one slot, or empty if there's no hashes.yaml or no entry for it. -// Loaded lazily from gAssetPath/hashes.yaml on the first call, then held for the run. -const std::string& GetBaselineAssetHash(uint32_t assetId); - // Classification of a >16MB BK ROM. enum class RomhackKind { NotRomhack, // vanilla-sized ROM @@ -37,11 +16,6 @@ enum class RomhackKind { UnknownRom, // extended ROM with no recognizable BK boot overlay }; -RomhackKind ClassifyRomhack(const std::vector& rom); - -// True for injected custom-code blobs and for CustomBuild ROMs. -bool HasCustomCodeBlob(const std::vector& rom); - // Binary aGameConfig format: 'BKCF' header + typed sections constexpr uint32_t GAMECONFIG_MAGIC = 0x46434B42; @@ -69,12 +43,6 @@ enum class CustomCodeKind : uint16_t { BB_INJECTED = 2, }; -// Pre-extraction probe for callers that decide the warning policy themselves -// (e.g. the extraction UI checks RomhackTable for ported hashes): reports -// the detected blob's kind and its SHA1 as 40 lowercase hex chars + NUL. -// Returns false (kind NONE, empty hash) when no blob is found. -bool GetCustomCodeBlobInfo(const std::vector& rom, CustomCodeKind& outKind, char outSha1Hex[41]); - // CODE_CONSTANTS key IDs - they index into the sBBConfigs table. enum class CodeConstantKey : uint16_t { NEW_GAME_MAP = 0, @@ -104,7 +72,44 @@ enum class CodeConstantKey : uint16_t { COUNT }; +// Vanilla BK is exactly 16MB. Anything bigger has been through Banjo's Backpack. +inline bool IsRomhack(const std::vector& rom) { + return rom.size() > 0x1000000; +} + +RomhackKind ClassifyRomhack(const std::vector& rom); + +// When a romhack, generate a config for it based on us v1.0. +bool TrySynthesizeRomConfig(YAML::Node& config, const std::string& cartHash, const std::filesystem::path& romPath, + const std::vector& rom); + // Build the binary aGameConfig blob from a BB romhack ROM. std::vector BuildGameConfigBlob(const std::vector& rom, const std::string& romName); +// True for injected custom-code blobs and for CustomBuild ROMs. +bool HasCustomCodeBlob(const std::vector& rom); + +// Pre-extraction probe for callers that decide the warning policy themselves +// (e.g. the extraction UI checks RomhackTable for ported hashes): reports +// the detected blob's kind and its SHA1 as 40 lowercase hex chars + NUL. +// Returns false (kind NONE, empty hash) when no blob is found. +bool GetCustomCodeBlobInfo(const std::vector& rom, CustomCodeKind& outKind, char outSha1Hex[41]); + +// Maintainer tool, creates a hashes.yml. +void EmitAssetHashes(const std::filesystem::path& romPath, const std::filesystem::path& outputPath); + +// How many slots differ from the baseline hashes - a missing baseline entry counts as +// changed. The port-side extractor uses it to size its progress bar to the real romhack +// delta instead of every vanilla slot. +size_t CountModifiedSlots(const std::vector& rom, const std::filesystem::path& hashesYamlPath); + +// Baseline SHA-1 for one slot, or empty if there's no hashes.yaml or no entry for it. +// Loaded lazily from gAssetPath/hashes.yaml on the first call, then held for the run. +const std::string& GetBaselineAssetHash(uint32_t assetId); + +// Vanilla warp destination (MAP << 8 | ENTRY) for a warp-table index, from the +// `warps:` section of hashes.yaml. Returns -1 if unknown. Loaded lazily like the +// baseline hashes. +int GetVanillaWarpDest(uint32_t warpIndex); + } // namespace BK64 diff --git a/src/factories/bk64/DemoInputFactory.cpp b/src/factories/bk64/DemoInputFactory.cpp index 702aec95..e797f9c8 100644 --- a/src/factories/bk64/DemoInputFactory.cpp +++ b/src/factories/bk64/DemoInputFactory.cpp @@ -6,24 +6,10 @@ #include "utils/Decompressor.h" #include "utils/TorchUtils.h" -#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec -#define FORMAT_HEX(x, w) \ - std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec +#include "BKEmitText.h" namespace BK64 { -ExportResult DemoInputHeaderExporter::Export(std::ostream& write, std::shared_ptr 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; - } - - return std::nullopt; -} - ExportResult DemoInputCodeExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { auto offset = GetSafeNode(node, "offset"); diff --git a/src/factories/bk64/DemoInputFactory.h b/src/factories/bk64/DemoInputFactory.h index 7b6efcc2..5cf922b8 100644 --- a/src/factories/bk64/DemoInputFactory.h +++ b/src/factories/bk64/DemoInputFactory.h @@ -1,6 +1,7 @@ #pragma once #include +#include "factories/bk64/BKHeaderExporter.h" namespace BK64 { @@ -20,11 +21,6 @@ class DemoInputData : public IParsedData { } }; -class DemoInputHeaderExporter : public BaseExporter { - ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, - YAML::Node& node, std::string* replacement) override; -}; - class DemoInputBinaryExporter : public BaseExporter { ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, YAML::Node& node, std::string* replacement) override; @@ -45,7 +41,7 @@ class DemoInputFactory : public BaseFactory { std::optional> parse(std::vector& buffer, YAML::Node& data) override; std::optional> parse_modding(std::vector& buffer, YAML::Node& data) override; inline std::unordered_map> GetExporters() override { - return { REGISTER(Code, DemoInputCodeExporter) REGISTER(Header, DemoInputHeaderExporter) + return { REGISTER(Code, DemoInputCodeExporter) REGISTER(Header, BKHeaderExporter) REGISTER(Binary, DemoInputBinaryExporter) REGISTER(Modding, DemoInputModdingExporter) }; } bool SupportModdedAssets() override { diff --git a/src/factories/bk64/DialogFactory.cpp b/src/factories/bk64/DialogFactory.cpp index 650f4306..b49c5420 100644 --- a/src/factories/bk64/DialogFactory.cpp +++ b/src/factories/bk64/DialogFactory.cpp @@ -1,5 +1,6 @@ #include "DialogFactory.h" +#include "BKDialogShared.h" #include "BKEmitText.h" #include "Companion.h" @@ -14,22 +15,13 @@ #define DIALOG_CMD_CLOSE 0x04 #define DIALOG_TERMINATOR_SIZE 3 -#define FORMAT_HEX(x, w) \ - std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec -#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec namespace BK64 { -ExportResult DialogHeaderExporter::Export(std::ostream& write, std::shared_ptr 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; - } - - return std::nullopt; +static void WriteLangBlock(LUS::BinaryWriter& writer, const std::vector& bottom, + const std::vector& top) { + WriteDialogStrings(writer, bottom); + WriteDialogStrings(writer, top); } ExportResult DialogCodeExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, @@ -47,58 +39,15 @@ ExportResult DialogCodeExporter::Export(std::ostream& write, std::shared_ptrmBottom.size() << ",\n"; - for (const auto [cmd, str] : dialog->mBottom) { - write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); - for (auto& c : str) { - if (c < ' ') { - write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); - } else if (c == '\'') { - write << ", \'\\" << c << "\'"; - } else { - write << ", \'" << c << "\'"; - } - } - write << ",\n"; - } + WriteDialogStringArray(write, dialog->mBottom); write << fourSpaceTab << "/* Top Dialog */\n"; - write << fourSpaceTab << dialog->mTop.size() << ",\n"; - for (const auto [cmd, str] : dialog->mTop) { - write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); - for (auto& c : str) { - if (c < ' ') { - write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); - } else if (c == '\'') { - write << ", \'\\" << c << "\'"; - } else { - write << ", \'" << c << "\'"; - } - } - write << ",\n"; - } + WriteDialogStringArray(write, dialog->mTop); write << "};\n\n"; return offset; } -static void WriteLangBlock(LUS::BinaryWriter& writer, const std::vector& bottom, - const std::vector& top) { - writer.Write((uint32_t)bottom.size()); - for (const auto& dialogString : bottom) { - writer.Write(dialogString.cmd); - writer.Write((uint32_t)dialogString.str.length()); - writer.Write((char*)dialogString.str.data(), dialogString.str.size()); - } - - writer.Write((uint32_t)top.size()); - for (const auto& dialogString : top) { - writer.Write(dialogString.cmd); - writer.Write((uint32_t)dialogString.str.length()); - writer.Write((char*)dialogString.str.data(), dialogString.str.size()); - } -} - ExportResult BK64::DialogBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { auto writer = LUS::BinaryWriter(); @@ -139,28 +88,12 @@ ExportResult BK64::DialogModdingExporter::Export(std::ostream& write, std::share out << YAML::Key << "Bottom"; out << YAML::Value; - out << YAML::BeginSeq; - for (const auto [cmd, str] : dialog->mBottom) { - out << YAML::Flow; - out << YAML::BeginSeq; - out << YAML_HEX((uint32_t)cmd); - EmitText(out, str); - out << YAML::EndSeq; - } - out << YAML::EndSeq; + EmitDialogStringSeq(out, dialog->mBottom); out << YAML::Key << "Top"; out << YAML::Value; - out << YAML::BeginSeq; - for (const auto [cmd, str] : dialog->mTop) { - out << YAML::Flow; - out << YAML::BeginSeq; - out << YAML_HEX((uint32_t)cmd); - EmitText(out, str); - out << YAML::EndSeq; - } - out << YAML::EndSeq; + EmitDialogStringSeq(out, dialog->mTop); out << YAML::EndMap; out << YAML::EndMap; @@ -177,10 +110,6 @@ static void EnsureTerminator(std::vector& box) { } } -static bool HasBytes(LUS::BinaryReader& reader, size_t count) { - return reader.GetBaseAddress() + count <= reader.GetLength(); -} - // Read one box's entries. A count byte the blob can't back would otherwise walk the reader off the // end, and whatever memory follows gets baked into the o2r as dialog text. Returns false once the // blob is exhausted so the caller stops rather than parsing the rest from nothing. @@ -293,40 +222,13 @@ std::optional> DialogFactory::parse(std::vector> DialogFactory::parse_modding(std::vector& buffer, YAML::Node& node) { - YAML::Node assetNode; - - try { - std::string text((char*)buffer.data(), buffer.size()); - assetNode = YAML::Load(text.c_str()); - } catch (YAML::ParserException& e) { - SPDLOG_ERROR("Failed to parse message data: {}", e.what()); - SPDLOG_ERROR("{}", (char*)buffer.data()); + const auto info = LoadModdingRoot(buffer); + if (!info.has_value()) { return std::nullopt; } - const auto info = assetNode.begin()->second; - - std::vector bottom; - std::vector top; - - auto bottomNode = info["Bottom"]; - auto topNode = info["Top"]; - - for (YAML::iterator it = bottomNode.begin(); it != bottomNode.end(); ++it) { - DialogString dialogString; - dialogString.cmd = (*it)[0].as(); - dialogString.str = DecodeText((*it)[1].as()); - dialogString.str += '\0'; - bottom.push_back(dialogString); - } - - for (YAML::iterator it = topNode.begin(); it != topNode.end(); ++it) { - DialogString dialogString; - dialogString.cmd = (*it)[0].as(); - dialogString.str = DecodeText((*it)[1].as()); - dialogString.str += '\0'; - top.push_back(dialogString); - } + auto bottom = ReadModdingDialogSeq((*info)["Bottom"]); + auto top = ReadModdingDialogSeq((*info)["Top"]); EnsureTerminator(bottom); EnsureTerminator(top); diff --git a/src/factories/bk64/DialogFactory.h b/src/factories/bk64/DialogFactory.h index 9398d641..e377f24e 100644 --- a/src/factories/bk64/DialogFactory.h +++ b/src/factories/bk64/DialogFactory.h @@ -1,6 +1,7 @@ #pragma once #include +#include "factories/bk64/BKHeaderExporter.h" namespace BK64 { @@ -33,11 +34,6 @@ class DialogData : public IParsedData { } }; -class DialogHeaderExporter : public BaseExporter { - ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, - YAML::Node& node, std::string* replacement) override; -}; - class DialogBinaryExporter : public BaseExporter { ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, YAML::Node& node, std::string* replacement) override; @@ -58,7 +54,7 @@ class DialogFactory : public BaseFactory { std::optional> parse(std::vector& buffer, YAML::Node& data) override; std::optional> parse_modding(std::vector& buffer, YAML::Node& data) override; inline std::unordered_map> GetExporters() override { - return { REGISTER(Code, DialogCodeExporter) REGISTER(Header, DialogHeaderExporter) + return { REGISTER(Code, DialogCodeExporter) REGISTER(Header, BKHeaderExporter) REGISTER(Binary, DialogBinaryExporter) REGISTER(Modding, DialogModdingExporter) }; } bool SupportModdedAssets() override { diff --git a/src/factories/bk64/GeoLayoutFactory.cpp b/src/factories/bk64/GeoLayoutFactory.cpp index e1e08fa1..b3a91b32 100644 --- a/src/factories/bk64/GeoLayoutFactory.cpp +++ b/src/factories/bk64/GeoLayoutFactory.cpp @@ -8,31 +8,10 @@ #include #include -#define ALIGN8(val) (((val) + 7) & ~7) -#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec +#include "BKEmitText.h" namespace BK64 { -ExportResult GeoLayoutHeaderExporter::Export(std::ostream& write, std::shared_ptr 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; - } - - return std::nullopt; -} - -ExportResult GeoLayoutCodeExporter::Export(std::ostream& write, std::shared_ptr raw, - std::string& entryName, YAML::Node& node, std::string* replacement) { - auto offset = GetSafeNode(node, "offset"); - auto geo = std::static_pointer_cast(raw); - - return offset; -} - // Serialized size of one geo command, 8-byte header included. static uint32_t GetGeoCommandByteSize(const GeoLayoutCommand& cmd) { uint32_t bodySize = 0; @@ -87,6 +66,14 @@ static uint32_t GetGeoCommandByteSize(const GeoLayoutCommand& cmd) { return 8 + bodySize; // 8 = opcode(4) + cmdLength(4) } +ExportResult GeoLayoutCodeExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode(node, "offset"); + auto geo = std::static_pointer_cast(raw); + + return offset; +} + ExportResult BK64::GeoLayoutBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { const auto geo = std::static_pointer_cast(raw); diff --git a/src/factories/bk64/GeoLayoutFactory.h b/src/factories/bk64/GeoLayoutFactory.h index 703f0e11..092d55f5 100644 --- a/src/factories/bk64/GeoLayoutFactory.h +++ b/src/factories/bk64/GeoLayoutFactory.h @@ -1,6 +1,7 @@ #pragma once #include +#include "factories/bk64/BKHeaderExporter.h" namespace BK64 { @@ -46,11 +47,6 @@ class GeoLayoutData : public IParsedData { } }; -class GeoLayoutHeaderExporter : public BaseExporter { - ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, - YAML::Node& node, std::string* replacement) override; -}; - class GeoLayoutBinaryExporter : public BaseExporter { ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, YAML::Node& node, std::string* replacement) override; @@ -72,7 +68,7 @@ class GeoLayoutFactory : public BaseFactory { // std::optional> // parse_modding(std::vector& buffer, YAML::Node& data) override; inline std::unordered_map> GetExporters() override { - return { REGISTER(Code, GeoLayoutCodeExporter) REGISTER(Header, GeoLayoutHeaderExporter) + return { REGISTER(Code, GeoLayoutCodeExporter) REGISTER(Header, BKHeaderExporter) REGISTER(Binary, GeoLayoutBinaryExporter) REGISTER(Modding, GeoLayoutModdingExporter) }; } bool SupportModdedAssets() override { diff --git a/src/factories/bk64/GruntyQuestionFactory.cpp b/src/factories/bk64/GruntyQuestionFactory.cpp index 49a47191..1e40e5ac 100644 --- a/src/factories/bk64/GruntyQuestionFactory.cpp +++ b/src/factories/bk64/GruntyQuestionFactory.cpp @@ -1,5 +1,6 @@ #include "GruntyQuestionFactory.h" +#include "BKDialogShared.h" #include "BKEmitText.h" #include "Companion.h" @@ -14,24 +15,68 @@ #define GRUNTY_QUESTION_HEADER_4 0x05 #define GRUNTY_QUESTION_HEADER_5 0x00 -#define FORMAT_HEX(x, w) \ - std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec -#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec - namespace BK64 { -ExportResult GruntyQuestionHeaderExporter::Export(std::ostream& write, std::shared_ptr raw, - std::string& entryName, YAML::Node& node, std::string* replacement) { - const auto symbol = GetSafeNode(node, "symbol", entryName); +namespace { - if (Companion::Instance->IsOTRMode()) { - write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; - return std::nullopt; +constexpr int8_t kUsHeader[3] = { GRUNTY_QUESTION_HEADER_1, GRUNTY_QUESTION_HEADER_2, GRUNTY_QUESTION_HEADER_3 }; +constexpr int8_t kPalHeader[3] = { 0x03, 0x03, 0x00 }; + +// One language's question data. Same shape for every language. Unlike quiz options, +// these split their leading control pair out of the string. +GruntyQuestionLang ParseGruntyBlock(LUS::BinaryReader& reader, const std::string& symbol) { + const int textSize = reader.ReadUByte(); + auto text = ReadDialogStrings(reader, textSize - kQuestionOptionCount, symbol, "GruntyQuestion"); + + std::vector options; + if (textSize >= kQuestionOptionCount) { + for (int i = 0; i < kQuestionOptionCount; i++) { + // cmd + length + the two control bytes, then the string past them + if (!HasBytes(reader, 4)) { + SPDLOG_WARN("[BK64] GruntyQuestion {}: option {} of {} runs past the end of the blob; dropping " + "the rest.", + symbol, i + 1, kQuestionOptionCount); + break; + } + + OptionString optionString; + optionString.cmd = reader.ReadUByte(); + const auto strLen = reader.ReadUByte(); + optionString.unk0 = reader.ReadUByte(); + optionString.unk1 = reader.ReadUByte(); + + if (strLen < 2 || !HasBytes(reader, strLen - 2)) { + SPDLOG_WARN("[BK64] GruntyQuestion {}: option {} of {} claims {} bytes the blob doesn't hold; " + "dropping the rest.", + symbol, i + 1, kQuestionOptionCount, strLen); + break; + } + + optionString.str = reader.ReadString(strLen - 2); + options.push_back(optionString); + } } - return std::nullopt; + return { std::move(text), std::move(options) }; +} + +void WriteGruntyLangBlock(LUS::BinaryWriter& writer, const std::vector& text, + const std::vector& options) { + WriteDialogStrings(writer, text); + + writer.Write((uint32_t)options.size()); + for (const auto& optionString : options) { + writer.Write(optionString.cmd); + writer.Write(optionString.unk0); + writer.Write(optionString.unk1); + writer.Write((uint32_t)optionString.str.length()); + // [port] Write(string) would prefix the length twice + writer.Write((char*)optionString.str.data(), optionString.str.size()); + } } +} // namespace + ExportResult GruntyQuestionCodeExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { auto offset = GetSafeNode(node, "offset"); @@ -51,34 +96,14 @@ ExportResult GruntyQuestionCodeExporter::Export(std::ostream& write, std::shared << "GRUNTY_QUESTION_HEADER_5" << ",\n"; write << fourSpaceTab << "/* GruntyQuestion */\n"; - write << fourSpaceTab << gruntyQuestion->mText.size() << ",\n"; - for (const auto [cmd, str] : gruntyQuestion->mText) { - write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); - for (auto& c : str) { - if (c < ' ') { - write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); - } else if (c == '\'') { - write << ", \'\\" << c << "\'"; - } else { - write << ", \'" << c << "\'"; - } - } - write << ",\n"; - } + WriteDialogStringArray(write, gruntyQuestion->mText); + write << fourSpaceTab << "/* Options */\n"; write << fourSpaceTab << gruntyQuestion->mOptions.size() << ",\n"; - for (const auto [cmd, unk0, unk1, str] : gruntyQuestion->mOptions) { + for (const auto& [cmd, unk0, unk1, str] : gruntyQuestion->mOptions) { write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); write << ", 0x" << FORMAT_HEX((uint32_t)unk0, 2) << ", 0x" << FORMAT_HEX((uint32_t)unk1, 2); - for (auto& c : str) { - if (c < ' ') { - write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); - } else if (c == '\'') { - write << ", \'\\" << c << "\'"; - } else { - write << ", \'" << c << "\'"; - } - } + WriteEscapedChars(write, str); write << ",\n"; } @@ -95,22 +120,16 @@ ExportResult BK64::GruntyQuestionBinaryExporter::Export(std::ostream& write, std WriteHeader(writer, Torch::ResourceType::BKGruntyQuestion, 0); - writer.Write((uint32_t)gruntyQuestion->mText.size()); - for (const auto& dialogString : gruntyQuestion->mText) { - writer.Write(dialogString.cmd); - writer.Write((uint32_t)dialogString.str.length()); - writer.Write((char*)dialogString.str.data(), - dialogString.str.size()); // [port] Write(string) would prefix the length twice - } + // 1 for US/JP, 3 for PAL (EN + FR + DE) + const uint32_t langCount = 1 + static_cast(gruntyQuestion->mExtraLangs.size()); + writer.Write(langCount); - writer.Write((uint32_t)gruntyQuestion->mOptions.size()); - for (const auto& optionString : gruntyQuestion->mOptions) { - writer.Write(optionString.cmd); - writer.Write(optionString.unk0); - writer.Write(optionString.unk1); - writer.Write((uint32_t)optionString.str.length()); - writer.Write((char*)optionString.str.data(), - optionString.str.size()); // [port] Write(string) would prefix the length twice + // English always goes first + WriteGruntyLangBlock(writer, gruntyQuestion->mText, gruntyQuestion->mOptions); + + // PAL only: French then German + for (const auto& lang : gruntyQuestion->mExtraLangs) { + WriteGruntyLangBlock(writer, lang.text, lang.options); } writer.Finish(write); @@ -134,22 +153,13 @@ ExportResult BK64::GruntyQuestionModdingExporter::Export(std::ostream& write, st out << YAML::BeginMap; out << YAML::Key << "Text"; out << YAML::Value; - - out << YAML::BeginSeq; - for (const auto [cmd, str] : gruntyQuestion->mText) { - out << YAML::Flow; - out << YAML::BeginSeq; - out << YAML_HEX((uint32_t)cmd); - EmitText(out, str); - out << YAML::EndSeq; - } - out << YAML::EndSeq; + EmitDialogStringSeq(out, gruntyQuestion->mText); out << YAML::Key << "Options"; out << YAML::Value; out << YAML::BeginSeq; - for (const auto [cmd, unk0, unk1, str] : gruntyQuestion->mOptions) { + for (const auto& [cmd, unk0, unk1, str] : gruntyQuestion->mOptions) { out << YAML::Flow; out << YAML::BeginSeq; out << YAML_HEX((uint32_t)cmd); @@ -168,100 +178,43 @@ ExportResult BK64::GruntyQuestionModdingExporter::Export(std::ostream& write, st return std::nullopt; } -// The actual question data. Same shape whether we got here via US or PAL English. -static std::shared_ptr ParseGruntyBlock(LUS::BinaryReader& reader) { - std::vector text; - std::vector options; - - auto textSize = reader.ReadUByte(); - - for (uint8_t i = 0; i < textSize - 3; i++) { - DialogString dialogString; - dialogString.cmd = reader.ReadUByte(); - auto strLen = reader.ReadUByte(); - dialogString.str = reader.ReadString(strLen); - text.push_back(dialogString); - } - - for (uint8_t i = textSize - 3; i < textSize; i++) { - OptionString optionString; - optionString.cmd = reader.ReadUByte(); - auto strLen = reader.ReadUByte(); - optionString.unk0 = reader.ReadUByte(); - optionString.unk1 = reader.ReadUByte(); - optionString.str = reader.ReadString(strLen - 2); - options.push_back(optionString); - } - - return std::make_shared(text, options); -} - std::optional> GruntyQuestionFactory::parse(std::vector& buffer, - YAML::Node& node) { + YAML::Node& node) { auto [_, segment] = Decompressor::AutoDecode(node, buffer); LUS::BinaryReader reader(segment.data, segment.size); reader.SetEndianness(Torch::Endianness::Big); const auto symbol = GetSafeNode(node, "symbol"); - auto header1 = reader.ReadInt8(); - auto header2 = reader.ReadInt8(); - auto header3 = reader.ReadInt8(); - - if (header1 == GRUNTY_QUESTION_HEADER_1 && header2 == GRUNTY_QUESTION_HEADER_2 && - header3 == GRUNTY_QUESTION_HEADER_3) { - // US: 01 03 00 05 00, then the question data - reader.ReadInt8(); // header4 (0x05) - reader.ReadInt8(); // header5 (0x00) - return ParseGruntyBlock(reader); + const auto extraOffsets = SeekQuestionBlock(reader, symbol, "GruntyQuestion", kUsHeader, kPalHeader); + if (!extraOffsets.has_value()) { + return std::nullopt; } - if (header1 == 0x03 && header2 == 0x03 && header3 == 0x00) { - // PAL: 03 03 00, then 3 x LE u16 offsets (EN/FR/DE start positions) - uint16_t enOffset = reader.ReadUByte() | (reader.ReadUByte() << 8); - reader.ReadUByte(); - reader.ReadUByte(); // skip FR offset - reader.ReadUByte(); - reader.ReadUByte(); // skip DE offset - // We're now at byte 9 = enOffset, so just read the English block. - return ParseGruntyBlock(reader); + auto english = ParseGruntyBlock(reader, symbol); + + std::vector extraLangs; + for (const uint16_t offset : *extraOffsets) { + reader.Seek(offset, LUS::SeekOffsetType::Start); + extraLangs.push_back(ParseGruntyBlock(reader, symbol)); } - SPDLOG_ERROR("Invalid Header For BK64 GruntyQuestion {}: {:02X} {:02X} {:02X}", symbol, header1, header2, header3); - return std::nullopt; + return std::make_shared(std::move(english.text), std::move(english.options), + std::move(extraLangs)); } std::optional> GruntyQuestionFactory::parse_modding(std::vector& buffer, YAML::Node& node) { - YAML::Node assetNode; - - try { - std::string text((char*)buffer.data(), buffer.size()); - assetNode = YAML::Load(text.c_str()); - } catch (YAML::ParserException& e) { - SPDLOG_ERROR("Failed to parse message data: {}", e.what()); - SPDLOG_ERROR("{}", (char*)buffer.data()); + const auto info = LoadModdingRoot(buffer); + if (!info.has_value()) { return std::nullopt; } - const auto info = assetNode.begin()->second; + auto text = ReadModdingDialogSeq((*info)["Text"]); - std::vector text; std::vector options; - - auto textNode = info["Text"]; - auto optionsNode = info["Options"]; - - for (YAML::iterator it = textNode.begin(); it != textNode.end(); ++it) { - DialogString dialogString; - dialogString.cmd = (*it)[0].as(); - dialogString.str = DecodeText((*it)[1].as()); - dialogString.str += '\0'; - text.push_back(dialogString); - } - - uint32_t i = 0; + auto optionsNode = (*info)["Options"]; for (YAML::iterator it = optionsNode.begin(); it != optionsNode.end(); ++it) { - if (i >= 3) { + if (options.size() >= kQuestionOptionCount) { SPDLOG_WARN("BK64 GruntyQuestion: Only 3 Options Allowed; extra options ignored"); break; } @@ -272,10 +225,9 @@ std::optional> GruntyQuestionFactory::parse_modding optionString.str = DecodeText((*it)[3].as()); optionString.str += '\0'; options.push_back(optionString); - i++; } - if (i != 3) { + if (options.size() != kQuestionOptionCount) { throw std::runtime_error("BK64 GruntyQuestion: Requires Exactly 3 Options"); } diff --git a/src/factories/bk64/GruntyQuestionFactory.h b/src/factories/bk64/GruntyQuestionFactory.h index da490ea9..061f1c0c 100644 --- a/src/factories/bk64/GruntyQuestionFactory.h +++ b/src/factories/bk64/GruntyQuestionFactory.h @@ -2,6 +2,7 @@ #include "DialogFactory.h" #include +#include "factories/bk64/BKHeaderExporter.h" namespace BK64 { @@ -12,19 +13,29 @@ typedef struct OptionString { std::string str; } OptionString; +// One language's question: the text entries plus the three answer options. +typedef struct GruntyQuestionLang { + std::vector text; + std::vector options; +} GruntyQuestionLang; + class GruntyQuestionData : public IParsedData { public: + // Always set. English on PAL, the lone language on US. std::vector mText; std::vector mOptions; + // PAL only: index 0=French, 1=German + std::vector mExtraLangs; + GruntyQuestionData(std::vector text, std::vector options) : mText(std::move(text)), mOptions(std::move(options)) { } -}; -class GruntyQuestionHeaderExporter : public BaseExporter { - ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, - YAML::Node& node, std::string* replacement) override; + GruntyQuestionData(std::vector text, std::vector options, + std::vector extraLangs) + : mText(std::move(text)), mOptions(std::move(options)), mExtraLangs(std::move(extraLangs)) { + } }; class GruntyQuestionBinaryExporter : public BaseExporter { @@ -47,7 +58,7 @@ class GruntyQuestionFactory : public BaseFactory { std::optional> parse(std::vector& buffer, YAML::Node& data) override; std::optional> parse_modding(std::vector& buffer, YAML::Node& data) override; inline std::unordered_map> GetExporters() override { - return { REGISTER(Code, GruntyQuestionCodeExporter) REGISTER(Header, GruntyQuestionHeaderExporter) + return { REGISTER(Code, GruntyQuestionCodeExporter) REGISTER(Header, BKHeaderExporter) REGISTER(Binary, GruntyQuestionBinaryExporter) REGISTER(Modding, GruntyQuestionModdingExporter) }; } bool SupportModdedAssets() override { diff --git a/src/factories/bk64/ModelFactory.cpp b/src/factories/bk64/ModelFactory.cpp index b0b3f6d1..565f06c8 100644 --- a/src/factories/bk64/ModelFactory.cpp +++ b/src/factories/bk64/ModelFactory.cpp @@ -12,6 +12,7 @@ #define GFX_HEADER_SIZE 0x8 #define GFX_CMD_SIZE 0x8 #define VTX_HEADER_SIZE 0x18 +#define N64_VTX_SIZE 16 #define ANIM_TEXTURE_LIST_COUNT 4 namespace BK64 { @@ -641,7 +642,6 @@ std::optional> ModelFactory::parse(std::vector(modelOffsetEnd - modelOffset); for (uint32_t candidate : { geoLayoutOffset, static_cast(textureSetupOffset), displayListSetupOffset, @@ -651,7 +651,7 @@ std::optional> ModelFactory::parse(std::vector(modelData->mVtxHeader.count)) { SPDLOG_DEBUG("[BKModel] {} vtx header count {} vs section-derived " "count {} — using section-derived", @@ -717,7 +717,6 @@ std::optional> ModelFactory::parse(std::vectormHasVtx && modelData->mHasDL && !modelData->mRawDLWords.empty()) { - constexpr uint32_t kN64VtxSize = 16; uint32_t maxVtxNeeded = modelData->mVtxHeader.count; for (size_t i = 0; i < modelData->mRawDLWords.size(); i += 2) { uint32_t w0 = modelData->mRawDLWords[i]; @@ -726,7 +725,7 @@ std::optional> ModelFactory::parse(std::vector> 10) & 0x3F; uint32_t off = SEGMENT_OFFSET(w1); - uint32_t vtxEnd = off / kN64VtxSize + n; + uint32_t vtxEnd = off / N64_VTX_SIZE + n; if (vtxEnd > maxVtxNeeded) { maxVtxNeeded = vtxEnd; } diff --git a/src/factories/bk64/QuizQuestionFactory.cpp b/src/factories/bk64/QuizQuestionFactory.cpp index 7ff94772..8ecc5020 100644 --- a/src/factories/bk64/QuizQuestionFactory.cpp +++ b/src/factories/bk64/QuizQuestionFactory.cpp @@ -1,5 +1,6 @@ #include "QuizQuestionFactory.h" +#include "BKDialogShared.h" #include "BKEmitText.h" #include "Companion.h" @@ -14,24 +15,32 @@ #define QUIZ_QUESTION_HEADER_4 0x05 #define QUIZ_QUESTION_HEADER_5 0x00 -#define FORMAT_HEX(x, w) \ - std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec -#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec - namespace BK64 { -ExportResult QuizQuestionHeaderExporter::Export(std::ostream& write, std::shared_ptr raw, - std::string& entryName, YAML::Node& node, std::string* replacement) { - const auto symbol = GetSafeNode(node, "symbol", entryName); +namespace { - if (Companion::Instance->IsOTRMode()) { - write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; - return std::nullopt; - } +constexpr int8_t kUsHeader[3] = { QUIZ_QUESTION_HEADER_1, QUIZ_QUESTION_HEADER_2, QUIZ_QUESTION_HEADER_3 }; +constexpr int8_t kPalHeader[3] = { 0x03, 0x01, 0x02 }; - return std::nullopt; +// One language's quiz data. Same layout for every language. The last three entries are +// the answer options; their first two bytes are a fixed control pair the game expects, +// so they stay part of the string. +QuizQuestionLang ParseQuizBlock(LUS::BinaryReader& reader, const std::string& symbol) { + const int textSize = reader.ReadUByte(); + auto text = ReadDialogStrings(reader, textSize - kQuestionOptionCount, symbol, "QuizQuestion"); + auto options = ReadDialogStrings(reader, textSize >= kQuestionOptionCount ? kQuestionOptionCount : 0, symbol, + "QuizQuestion option"); + return { std::move(text), std::move(options) }; } +void WriteQuizLangBlock(LUS::BinaryWriter& writer, const std::vector& text, + const std::vector& options) { + WriteDialogStrings(writer, text); + WriteDialogStrings(writer, options); +} + +} // namespace + ExportResult QuizQuestionCodeExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { auto offset = GetSafeNode(node, "offset"); @@ -51,35 +60,9 @@ ExportResult QuizQuestionCodeExporter::Export(std::ostream& write, std::shared_p << "QUIZ_QUESTION_HEADER_5" << ",\n"; write << fourSpaceTab << "/* QuizQuestion */\n"; - write << fourSpaceTab << quizQuestion->mText.size() << ",\n"; - for (const auto [cmd, str] : quizQuestion->mText) { - write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); - for (auto& c : str) { - if (c < ' ') { - write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); - } else if (c == '\'') { - write << ", \'\\" << c << "\'"; - } else { - write << ", \'" << c << "\'"; - } - } - write << ",\n"; - } + WriteDialogStringArray(write, quizQuestion->mText); write << fourSpaceTab << "/* Options */\n"; - write << fourSpaceTab << quizQuestion->mOptions.size() << ",\n"; - for (const auto [cmd, str] : quizQuestion->mOptions) { - write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); - for (auto& c : str) { - if (c < ' ') { - write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); - } else if (c == '\'') { - write << ", \'\\" << c << "\'"; - } else { - write << ", \'" << c << "\'"; - } - } - write << ",\n"; - } + WriteDialogStringArray(write, quizQuestion->mOptions); write << "};\n\n"; @@ -94,20 +77,16 @@ ExportResult BK64::QuizQuestionBinaryExporter::Export(std::ostream& write, std:: WriteHeader(writer, Torch::ResourceType::BKQuizQuestion, 0); - writer.Write((uint32_t)quizQuestion->mText.size()); - for (const auto& dialogString : quizQuestion->mText) { - writer.Write(dialogString.cmd); - writer.Write((uint32_t)dialogString.str.length()); - writer.Write((char*)dialogString.str.data(), - dialogString.str.size()); // [port] Write(string) would prefix the length twice - } + // 1 for US/JP, 3 for PAL (EN + FR + DE) + const uint32_t langCount = 1 + static_cast(quizQuestion->mExtraLangs.size()); + writer.Write(langCount); + + // English always goes first + WriteQuizLangBlock(writer, quizQuestion->mText, quizQuestion->mOptions); - writer.Write((uint32_t)quizQuestion->mOptions.size()); - for (const auto& optionString : quizQuestion->mOptions) { - writer.Write(optionString.cmd); - writer.Write((uint32_t)optionString.str.length()); - writer.Write((char*)optionString.str.data(), - optionString.str.size()); // [port] Write(string) would prefix the length twice + // PAL only: French then German + for (const auto& lang : quizQuestion->mExtraLangs) { + WriteQuizLangBlock(writer, lang.text, lang.options); } writer.Finish(write); @@ -131,22 +110,15 @@ ExportResult BK64::QuizQuestionModdingExporter::Export(std::ostream& write, std: out << YAML::BeginMap; out << YAML::Key << "Text"; out << YAML::Value; - - out << YAML::BeginSeq; - for (const auto [cmd, str] : quizQuestion->mText) { - out << YAML::Flow; - out << YAML::BeginSeq; - out << YAML_HEX((uint32_t)cmd); - EmitText(out, str); - out << YAML::EndSeq; - } - out << YAML::EndSeq; + EmitDialogStringSeq(out, quizQuestion->mText); out << YAML::Key << "Options"; out << YAML::Value; + // Options expose their leading control pair as separate fields so a pack can + // edit the text without touching them. out << YAML::BeginSeq; - for (const auto [cmd, str] : quizQuestion->mOptions) { + for (const auto& [cmd, str] : quizQuestion->mOptions) { out << YAML::Flow; out << YAML::BeginSeq; out << YAML_HEX((uint32_t)cmd); @@ -165,97 +137,42 @@ ExportResult BK64::QuizQuestionModdingExporter::Export(std::ostream& write, std: return std::nullopt; } -// The quiz data itself. Same layout for US and for PAL English. -static std::shared_ptr ParseQuizBlock(LUS::BinaryReader& reader) { - std::vector text; - std::vector options; - - auto textSize = reader.ReadUByte(); - - for (uint8_t i = 0; i < textSize - 3; i++) { - DialogString dialogString; - dialogString.cmd = reader.ReadUByte(); - auto strLen = reader.ReadUByte(); - dialogString.str = reader.ReadString(strLen); - text.push_back(dialogString); - } - - for (uint8_t i = textSize - 3; i < textSize; i++) { - DialogString optionString; - optionString.cmd = reader.ReadUByte(); - auto strLen = reader.ReadUByte(); - optionString.str = reader.ReadString(strLen); - options.push_back(optionString); - } - - return std::make_shared(text, options); -} - std::optional> QuizQuestionFactory::parse(std::vector& buffer, YAML::Node& node) { auto [_, segment] = Decompressor::AutoDecode(node, buffer); LUS::BinaryReader reader(segment.data, segment.size); reader.SetEndianness(Torch::Endianness::Big); const auto symbol = GetSafeNode(node, "symbol"); - auto header1 = reader.ReadInt8(); - auto header2 = reader.ReadInt8(); - auto header3 = reader.ReadInt8(); - - if (header1 == QUIZ_QUESTION_HEADER_1 && header2 == QUIZ_QUESTION_HEADER_2 && header3 == QUIZ_QUESTION_HEADER_3) { - // US: 01 01 02 05 00, then quiz data - reader.ReadInt8(); // header4 (0x05) - reader.ReadInt8(); // header5 (0x00) - return ParseQuizBlock(reader); + const auto extraOffsets = SeekQuestionBlock(reader, symbol, "QuizQuestion", kUsHeader, kPalHeader); + if (!extraOffsets.has_value()) { + return std::nullopt; } - if (header1 == 0x03 && header2 == 0x01 && header3 == 0x02) { - // PAL: 03 01 02, then 3 x LE u16 offsets (EN/FR/DE start positions). - // The English block lives at the first of those offsets. - uint16_t enOffset = reader.ReadUByte() | (reader.ReadUByte() << 8); - reader.ReadUByte(); - reader.ReadUByte(); // skip FR offset - reader.ReadUByte(); - reader.ReadUByte(); // skip DE offset - // We're now at byte 9 = enOffset, so just read the English block. - return ParseQuizBlock(reader); + auto english = ParseQuizBlock(reader, symbol); + + std::vector extraLangs; + for (const uint16_t offset : *extraOffsets) { + reader.Seek(offset, LUS::SeekOffsetType::Start); + extraLangs.push_back(ParseQuizBlock(reader, symbol)); } - SPDLOG_ERROR("Invalid Header For BK64 QuizQuestion {}: {:02X} {:02X} {:02X}", symbol, header1, header2, header3); - return std::nullopt; + return std::make_shared(std::move(english.text), std::move(english.options), + std::move(extraLangs)); } std::optional> QuizQuestionFactory::parse_modding(std::vector& buffer, - YAML::Node& node) { - YAML::Node assetNode; - - try { - std::string text((char*)buffer.data(), buffer.size()); - assetNode = YAML::Load(text.c_str()); - } catch (YAML::ParserException& e) { - SPDLOG_ERROR("Failed to parse message data: {}", e.what()); - SPDLOG_ERROR("{}", (char*)buffer.data()); + YAML::Node& node) { + const auto info = LoadModdingRoot(buffer); + if (!info.has_value()) { return std::nullopt; } - const auto info = assetNode.begin()->second; + auto text = ReadModdingDialogSeq((*info)["Text"]); - std::vector text; std::vector options; - - auto textNode = info["Text"]; - auto optionsNode = info["Options"]; - - for (YAML::iterator it = textNode.begin(); it != textNode.end(); ++it) { - DialogString dialogString; - dialogString.cmd = (*it)[0].as(); - dialogString.str = DecodeText((*it)[1].as()); - dialogString.str += '\0'; - text.push_back(dialogString); - } - - uint32_t i = 0; + auto optionsNode = (*info)["Options"]; for (YAML::iterator it = optionsNode.begin(); it != optionsNode.end(); ++it) { - if (i >= 3) { + if (options.size() >= 3) { SPDLOG_WARN("BK64 QuizQuestion: Only 3 Options Allowed; extra options ignored"); break; } @@ -269,10 +186,9 @@ std::optional> QuizQuestionFactory::parse_modding(s optionString.str += DecodeText((*it)[3].as()); optionString.str += '\0'; options.push_back(optionString); - i++; } - if (i != 3) { + if (options.size() != 3) { throw std::runtime_error("BK64 QuizQuestion: Requires Exactly 3 Options"); } diff --git a/src/factories/bk64/QuizQuestionFactory.h b/src/factories/bk64/QuizQuestionFactory.h index 2c9598c0..be68be59 100644 --- a/src/factories/bk64/QuizQuestionFactory.h +++ b/src/factories/bk64/QuizQuestionFactory.h @@ -2,22 +2,33 @@ #include "DialogFactory.h" #include +#include "factories/bk64/BKHeaderExporter.h" namespace BK64 { +// One language's question: the text entries plus the three answer options. +typedef struct QuizQuestionLang { + std::vector text; + std::vector options; +} QuizQuestionLang; + class QuizQuestionData : public IParsedData { public: + // Always set. English on PAL, the lone language on US. std::vector mText; std::vector mOptions; + // PAL only: index 0=French, 1=German + std::vector mExtraLangs; + QuizQuestionData(std::vector text, std::vector options) : mText(std::move(text)), mOptions(std::move(options)) { } -}; -class QuizQuestionHeaderExporter : public BaseExporter { - ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, - YAML::Node& node, std::string* replacement) override; + QuizQuestionData(std::vector text, std::vector options, + std::vector extraLangs) + : mText(std::move(text)), mOptions(std::move(options)), mExtraLangs(std::move(extraLangs)) { + } }; class QuizQuestionBinaryExporter : public BaseExporter { @@ -40,7 +51,7 @@ class QuizQuestionFactory : public BaseFactory { std::optional> parse(std::vector& buffer, YAML::Node& data) override; std::optional> parse_modding(std::vector& buffer, YAML::Node& data) override; inline std::unordered_map> GetExporters() override { - return { REGISTER(Code, QuizQuestionCodeExporter) REGISTER(Header, QuizQuestionHeaderExporter) + return { REGISTER(Code, QuizQuestionCodeExporter) REGISTER(Header, BKHeaderExporter) REGISTER(Binary, QuizQuestionBinaryExporter) REGISTER(Modding, QuizQuestionModdingExporter) }; } bool SupportModdedAssets() override { diff --git a/src/factories/bk64/SoundfontTblFactory.cpp b/src/factories/bk64/SoundfontTblFactory.cpp index 4bfec77f..9b4d08e9 100644 --- a/src/factories/bk64/SoundfontTblFactory.cpp +++ b/src/factories/bk64/SoundfontTblFactory.cpp @@ -1,4 +1,5 @@ #include "SoundfontTblFactory.h" +#include "BKByteUtils.h" #include "Companion.h" #include "spdlog/spdlog.h" @@ -14,18 +15,8 @@ namespace BK64 { namespace { -uint16_t ReadU16BE(const uint8_t* p) { - return (uint16_t)((p[0] << 8) | p[1]); -} -int16_t ReadS16BE(const uint8_t* p) { - return (int16_t)ReadU16BE(p); -} -uint32_t ReadU32BE(const uint8_t* p) { - return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3]; -} -int32_t ReadS32BE(const uint8_t* p) { - return (int32_t)ReadU32BE(p); -} +// AL_BANK_VERSION magic +constexpr uint16_t kAlBankRevision = 0x4231; // Walk an N64 ALBankFile binary (big-endian, 32-bit offsets relative to ctl // start) and return max(wavetable.base + wavetable.len) over every wavetable @@ -116,9 +107,6 @@ size_t ComputeTblSize(const uint8_t* ctl, size_t ctlSize, uint32_t ctlRomOffset, return (size_t)((maxEnd + 0xF) & ~(uint64_t)0xF); } -// AL_BANK_VERSION magic -constexpr uint16_t kAlBankRevision = 0x4231; - bool ValidateCtl(const uint8_t* ctl, size_t ctlSize) { if (ctlSize < 8) { return false; diff --git a/src/factories/bk64/SpriteFactory.cpp b/src/factories/bk64/SpriteFactory.cpp index 0dd065e2..e491c5e1 100644 --- a/src/factories/bk64/SpriteFactory.cpp +++ b/src/factories/bk64/SpriteFactory.cpp @@ -65,18 +65,6 @@ void ExtractChunk(LUS::BinaryReader& reader, std::vector 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; - } - - return std::nullopt; -} - ExportResult SpriteCodeExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { auto sprite = std::static_pointer_cast(raw); diff --git a/src/factories/bk64/SpriteFactory.h b/src/factories/bk64/SpriteFactory.h index d00b7a4d..ba6a07a2 100644 --- a/src/factories/bk64/SpriteFactory.h +++ b/src/factories/bk64/SpriteFactory.h @@ -1,6 +1,7 @@ #pragma once #include "factories/BaseFactory.h" +#include "factories/bk64/BKHeaderExporter.h" #include "utils/TextureUtils.h" #include #include @@ -56,11 +57,6 @@ class SpriteData : public IParsedData { } }; -class SpriteHeaderExporter : public BaseExporter { - ExportResult Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, - std::string* replacement) override; -}; - class SpriteBinaryExporter : public BaseExporter { ExportResult Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) override; @@ -81,7 +77,7 @@ class SpriteFactory : public BaseFactory { std::optional> parse(std::vector& buffer, YAML::Node& data) override; std::optional> parse_modding(std::vector& buffer, YAML::Node& data) override; inline std::unordered_map> GetExporters() override { - return { REGISTER(Header, SpriteHeaderExporter) REGISTER(Binary, SpriteBinaryExporter) + return { REGISTER(Header, BKHeaderExporter) REGISTER(Binary, SpriteBinaryExporter) REGISTER(Code, SpriteCodeExporter) REGISTER(Modding, SpriteModdingExporter) }; }