diff options
| -rw-r--r-- | src/factories/bk64/DialogFactory.cpp | 103 |
1 files changed, 73 insertions, 30 deletions
diff --git a/src/factories/bk64/DialogFactory.cpp b/src/factories/bk64/DialogFactory.cpp index 4a1ef30..e4aa925 100644 --- a/src/factories/bk64/DialogFactory.cpp +++ b/src/factories/bk64/DialogFactory.cpp @@ -9,6 +9,8 @@ #define DIALOG_HEADER_1 0x01 #define DIALOG_HEADER_2 0x03 #define DIALOG_HEADER_3 0x00 +#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 @@ -166,44 +168,82 @@ ExportResult BK64::DialogModdingExporter::Export(std::ostream& write, std::share return std::nullopt; } -// One language: bottom box strings, then top box strings. -static DialogLang ParseLangBlock(LUS::BinaryReader& reader) { - DialogLang lang; +// Give empty dialogs the terminator the game expects +static void EnsureTerminator(std::vector<DialogString>& box) { + if (box.empty()) { + box.push_back({ DIALOG_CMD_CLOSE, std::string(1, '\0') }); + } +} + +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. +static bool ReadEntries(LUS::BinaryReader& reader, uint8_t count, std::vector<DialogString>& box, + const std::string& symbol, const char* boxName) { + for (uint8_t i = 0; i < count; i++) { + // cmd + length, then the string itself + if (!HasBytes(reader, 2)) { + SPDLOG_WARN("[BK64] Dialog {}: {} entry {} of {} runs past the end of the blob; dropping the rest.", + symbol, boxName, i + 1, count); + return false; + } - auto bottomSize = reader.ReadUByte(); - for (uint8_t i = 0; i < bottomSize; i++) { DialogString dialogString; dialogString.cmd = reader.ReadUByte(); auto strLen = reader.ReadUByte(); + + if (!HasBytes(reader, strLen)) { + SPDLOG_WARN("[BK64] Dialog {}: {} entry {} of {} claims {} bytes the blob doesn't hold; dropping the rest.", + symbol, boxName, i + 1, count, strLen); + return false; + } + dialogString.str = reader.ReadString(strLen); - lang.bottom.push_back(dialogString); + box.push_back(dialogString); } + return true; +} - // Banjo's Backpack writes a 04 01 00 separator between bottom and top - // sections unconditionally, even when bottomSize is 0. When present, - // skip it so topSize reads correctly. Only check after an empty bottom - // to avoid false matches in vanilla dialogs. - if (bottomSize == 0) { - uint32_t savedPos = reader.GetBaseAddress(); - if (savedPos + 3 <= reader.GetLength()) { - uint8_t b0 = reader.ReadUByte(); - uint8_t b1 = reader.ReadUByte(); - uint8_t b2 = reader.ReadUByte(); - if (b0 != 0x04 || b1 != 0x01 || b2 != 0x00) { - reader.Seek(savedPos, LUS::SeekOffsetType::Start); - } +// One language: bottom box strings, then top box strings. +static DialogLang ParseLangBlock(LUS::BinaryReader& reader, const std::string& symbol) { + DialogLang lang; + + uint8_t bottomSize = 0; + bool bottomComplete = false; + + if (HasBytes(reader, 1)) { + bottomSize = reader.ReadUByte(); + bottomComplete = ReadEntries(reader, bottomSize, lang.bottom, symbol, "bottom"); + } else { + SPDLOG_WARN("[BK64] Dialog {}: blob ends before the bottom box count.", symbol); + } + + // Banjo's Backpack still writes the bottom box's terminator entry after a zero count. Consume + // it or it gets read as topSize (0x04), inventing four junk top entries and running off the + // blob; EnsureTerminator puts the entry back below. A real top box can't open with these bytes. + if (bottomComplete && bottomSize == 0 && HasBytes(reader, DIALOG_TERMINATOR_SIZE)) { + const uint32_t savedPos = reader.GetBaseAddress(); + const uint8_t b0 = reader.ReadUByte(); + const uint8_t b1 = reader.ReadUByte(); + const uint8_t b2 = reader.ReadUByte(); + if (b0 != DIALOG_CMD_CLOSE || b1 != 0x01 || b2 != 0x00) { + reader.Seek(savedPos, LUS::SeekOffsetType::Start); } } - auto topSize = reader.ReadUByte(); - for (uint8_t i = 0; i < topSize; i++) { - DialogString dialogString; - dialogString.cmd = reader.ReadUByte(); - auto strLen = reader.ReadUByte(); - dialogString.str = reader.ReadString(strLen); - lang.top.push_back(dialogString); + // A truncated bottom means every offset past it is guesswork, so don't invent a top from it. + if (bottomComplete && HasBytes(reader, 1)) { + const uint8_t topSize = reader.ReadUByte(); + ReadEntries(reader, topSize, lang.top, symbol, "top"); } + EnsureTerminator(lang.bottom); + EnsureTerminator(lang.top); + return lang; } @@ -219,7 +259,7 @@ std::optional<std::shared_ptr<IParsedData>> DialogFactory::parse(std::vector<uin if (header1 == DIALOG_HEADER_1 && header2 == DIALOG_HEADER_2 && header3 == DIALOG_HEADER_3) { // US/JP: 01 03 00, dialog data follows immediately - auto lang = ParseLangBlock(reader); + auto lang = ParseLangBlock(reader, symbol); return std::make_shared<DialogData>(std::move(lang.bottom), std::move(lang.top)); } @@ -230,13 +270,13 @@ std::optional<std::shared_ptr<IParsedData>> DialogFactory::parse(std::vector<uin uint16_t germanOffset = reader.ReadUByte() | (reader.ReadUByte() << 8); // EN sits right here at byte 7; FR and DE we seek to. - auto english = ParseLangBlock(reader); + auto english = ParseLangBlock(reader, symbol); reader.Seek(frenchOffset, LUS::SeekOffsetType::Start); - auto french = ParseLangBlock(reader); + auto french = ParseLangBlock(reader, symbol); reader.Seek(germanOffset, LUS::SeekOffsetType::Start); - auto german = ParseLangBlock(reader); + auto german = ParseLangBlock(reader, symbol); std::vector<DialogLang> extraLangs; extraLangs.push_back(std::move(french)); @@ -286,6 +326,9 @@ std::optional<std::shared_ptr<IParsedData>> DialogFactory::parse_modding(std::ve top.push_back(dialogString); } + EnsureTerminator(bottom); + EnsureTerminator(top); + return std::make_shared<DialogData>(bottom, top); } |
