diff --git a/.gitignore b/.gitignore index 622cc72..a8a3d88 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ cmake-build-* BMEdit/Editor/UI/UI/*.autosave ThirdParty/zlib/zconf.h.included Test -venv \ No newline at end of file +venv +Notes.txt \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp b/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp index 4260993..0c3e4b8 100644 --- a/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp +++ b/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp @@ -43,6 +43,11 @@ namespace models return QString::fromStdString(node->value); } + if (index.column() == 1 && node->type == gamelib::loc::LOCTreeNodeType::EMPTY_BLOCK) + { + return QString("(EMPTY)"); + } + return {}; } @@ -199,11 +204,19 @@ namespace models if (!node) return flags; - if (index.column() == 0 || (index.column() == 1 && node->canHaveValue())) + if (index.column() == 0) { flags |= Qt::ItemIsEditable; } + if (index.column() == 1) + { + if (node->canHaveValue()) + { + flags |= Qt::ItemIsEditable; + } + } + return flags; } diff --git a/BMEdit/GameLib/Include/GameLib/LOC/LOCTreeNode.h b/BMEdit/GameLib/Include/GameLib/LOC/LOCTreeNode.h index 2e26fad..5b0083b 100644 --- a/BMEdit/GameLib/Include/GameLib/LOC/LOCTreeNode.h +++ b/BMEdit/GameLib/Include/GameLib/LOC/LOCTreeNode.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace ZBio::ZBinaryReader @@ -23,7 +24,9 @@ namespace gamelib::loc EMPTY_BLOCK = 0x8, ///< Empty chunk, no data at all LOCALIZED_STRING = 0x9, ///< Key value (string to aligned string) CHILDREN = 0x10, ///< Container (amount & list of offsets) + SUBTITLES_FIN = 0x28, ///< Subtitles finish string. NOTE: Maybe it's finish subtitle, will rename it later SUBTITLES = 0x29, ///< Subtitles value (long text with extra parameters) | 0x20 mask means that extra data exists + SUBTITLES_HINT = 0x2B ///< Another subtitles data with extra string hint }; enum LOCMissionObjectiveType : char @@ -66,12 +69,13 @@ namespace gamelib::loc struct SubtitleData { uint8_t unkData[8]; + std::string extraHint {}; } subtitle; [[nodiscard]] bool canHaveValue() const; [[nodiscard]] bool canHaveChildren() const; static void deserialize(const LOCTreeNode::Ptr &node, ZBio::ZBinaryReader::BinaryReader* binaryReader); - static void serialize(const LOCTreeNode::Ptr& node, ZBio::ZBinaryWriter::BinaryWriter* binaryWriter); + static void serialize(const LOCTreeNode::Ptr& node, ZBio::ZBinaryWriter::BinaryWriter* binaryWriter, std::vector>& replacement); }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/LOC/LOCTreeNode.cpp b/BMEdit/GameLib/Source/GameLib/LOC/LOCTreeNode.cpp index 79f4ec8..cd97584 100644 --- a/BMEdit/GameLib/Source/GameLib/LOC/LOCTreeNode.cpp +++ b/BMEdit/GameLib/Source/GameLib/LOC/LOCTreeNode.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -9,7 +10,11 @@ namespace gamelib::loc { bool LOCTreeNode::canHaveValue() const { - return type == LOCTreeNodeType::LOCALIZED_STRING || type == LOCTreeNodeType::SUBTITLES; + return + type == LOCTreeNodeType::LOCALIZED_STRING || + type == LOCTreeNodeType::SUBTITLES || + type == LOCTreeNodeType::SUBTITLES_HINT || + type == LOCTreeNodeType::SUBTITLES_FIN; } bool LOCTreeNode::canHaveChildren() const @@ -23,9 +28,12 @@ namespace gamelib::loc node->name = binaryReader->readCString(); const auto type = binaryReader->read(); - if (type != LOCTreeNodeType::CHILDREN && type != LOCTreeNodeType::LOCALIZED_STRING && type != LOCTreeNodeType::SUBTITLES && type != LOCTreeNodeType::EMPTY_BLOCK) + if (type != LOCTreeNodeType::CHILDREN && type != LOCTreeNodeType::LOCALIZED_STRING && + type != LOCTreeNodeType::SUBTITLES && type != LOCTreeNodeType::EMPTY_BLOCK && + type != LOCTreeNodeType::SUBTITLES_FIN && type != LOCTreeNodeType::SUBTITLES_HINT) { - throw std::runtime_error("Invalid LOC format: expected to have 0x0 or 0x10, got smth else"); + auto errorMessage = fmt::format("Invalid LOC format: unexpected entity code 0x{:02X} at offset {} (0x{:X})", type, binaryReader->tell(), binaryReader->tell()); + throw std::runtime_error(errorMessage); } // Store type @@ -72,13 +80,38 @@ namespace gamelib::loc ZBioHelpers::seekBy(binaryReader, 4); // Need seek by 4 because value strings are "aligned". // Formula: len + 1 + 4 (+1 - zero terminator, 4 - "alignment") } - else if (node->type == LOCTreeNodeType::SUBTITLES) + else if (node->type == LOCTreeNodeType::SUBTITLES || node->type == LOCTreeNodeType::SUBTITLES_HINT || node->type == LOCTreeNodeType::SUBTITLES_FIN) { // Subtitles text node->value = binaryReader->readCString(); - // Read subtitle data. I really don't know what that value means, but as 2xu32 each same to each - binaryReader->read(&node->subtitle.unkData[0], 8); + if (node->type == LOCTreeNodeType::SUBTITLES_HINT) + { + // Read extra hint string + node->subtitle.extraHint = binaryReader->readCString(); + } + + if (node->type == LOCTreeNodeType::SUBTITLES || node->type == LOCTreeNodeType::SUBTITLES_HINT) + { + // Read subtitle data. In most cases there are 2xu32, but when first u32 zeroed next u32 not presented + // I love IOI because they don't give me an opportunity to relax... + uint32_t first = binaryReader->read(); + uint32_t second = 0; + if (first != 0) + { + // Ok, read second + second = binaryReader->read(); + } + + *reinterpret_cast(&node->subtitle.unkData[0]) = first; + *reinterpret_cast(&node->subtitle.unkData[4]) = second; + } + + if (node->type == LOCTreeNodeType::SUBTITLES_FIN) + { + // Always only 1 u32 + binaryReader->read(&node->subtitle.unkData[0], 4); + } } else if (node->type == LOCTreeNodeType::EMPTY_BLOCK) { @@ -90,7 +123,7 @@ namespace gamelib::loc } } - void LOCTreeNode::serialize(const LOCTreeNode::Ptr &node, ZBio::ZBinaryWriter::BinaryWriter *binaryWriter) // NOLINT(*-no-recursion) + void LOCTreeNode::serialize(const LOCTreeNode::Ptr &node, ZBio::ZBinaryWriter::BinaryWriter *binaryWriter, std::vector>& replacement) // NOLINT(*-no-recursion) { // Write name (not aligned) binaryWriter->writeCString(node->name); @@ -102,19 +135,19 @@ namespace gamelib::loc { // Sort children auto children = node->children; // Need copy - std::sort(children.begin(), children.end(), [](const LOCTreeNode::Ptr& a, const LOCTreeNode::Ptr& b) { return a->name < b->name; }); + //std::sort(children.begin(), children.end(), [](const LOCTreeNode::Ptr& a, const LOCTreeNode::Ptr& b) { return a->name < b->name; }); // Write count const auto childrenCount = children.size() > 0xFF ? 0xFF : static_cast(children.size()); binaryWriter->write(childrenCount); // Write offsets - std::vector offsets{}; + std::vector offsets { 0u }; for (int i = 1; i < childrenCount; i++) { offsets.push_back(binaryWriter->tell()); - binaryWriter->write(0); // Fake offset (will be fixed) + binaryWriter->write(0xDDDDDDDDu); } // Write node by node & restore offsets @@ -124,18 +157,13 @@ namespace gamelib::loc { if (i > 0) { - // Restore offset + // Save replacement instruction uint32_t newOffset = binaryWriter->tell(); - - ZBioSeekGuard guard { binaryWriter }; - binaryWriter->seek(offsets[i - 1]); - - // Update offset - binaryWriter->write(newOffset - baseOffset); // Offset calculated from node #0 + replacement.emplace_back(offsets[i], newOffset - baseOffset); } // Write node itself - LOCTreeNode::serialize(children[i], binaryWriter); + LOCTreeNode::serialize(children[i], binaryWriter, replacement); } } else if (node->type == LOCTreeNodeType::LOCALIZED_STRING) @@ -146,13 +174,29 @@ namespace gamelib::loc // Write 4 byte alignment binaryWriter->write(0); } - else if (node->type == LOCTreeNodeType::SUBTITLES) + else if (node->type == LOCTreeNodeType::SUBTITLES || node->type == LOCTreeNodeType::SUBTITLES_HINT) { // Write unaligned string binaryWriter->writeCString(node->value); + if (node->type == LOCTreeNodeType::SUBTITLES_HINT) + { + // For hinted string need to write extra hint + binaryWriter->writeCString(node->subtitle.extraHint); + } + + uint32_t first = *reinterpret_cast(&node->subtitle.unkData[0]); + uint32_t second = *reinterpret_cast(&node->subtitle.unkData[4]); + // Write subtitles data - binaryWriter->write(&node->subtitle.unkData[0], 8); + binaryWriter->write(first); + if (first) + binaryWriter->write(second); + } + else if (node->type == LOCTreeNodeType::SUBTITLES_FIN) + { + // Store tutorial data here + binaryWriter->write(&node->subtitle.unkData[0], 4); } else if (node->type == LOCTreeNodeType::EMPTY_BLOCK) { diff --git a/BMEdit/GameLib/Source/GameLib/LOC/LOCWriter.cpp b/BMEdit/GameLib/Source/GameLib/LOC/LOCWriter.cpp index 59aa01e..459157e 100644 --- a/BMEdit/GameLib/Source/GameLib/LOC/LOCWriter.cpp +++ b/BMEdit/GameLib/Source/GameLib/LOC/LOCWriter.cpp @@ -12,21 +12,23 @@ namespace gamelib::loc auto writerSink = std::make_unique(); auto binaryWriter = ZBio::ZBinaryWriter::BinaryWriter(std::move(writerSink)); + std::vector> replacement {}; + // Sort children by name. It's required because game wants to interact with sorted tree auto children = root->children; - std::sort(children.begin(), children.end(), [](const LOCTreeNode::Ptr& a, const LOCTreeNode::Ptr& b) { return a->name < b->name; }); + //std::sort(children.begin(), children.end(), [](const LOCTreeNode::Ptr& a, const LOCTreeNode::Ptr& b) { return a->name < b->name; }); // Write base binaryWriter.write(static_cast(children.size())); - std::vector offsets {}; + std::vector offsets { 0u }; // zero stored by default // Fill offsets if root child more than 1 for (int i = 1; i < children.size(); i++) { // Here we need to know an offset of subject after first created node. Instead of that we will save current offset and then jump back offsets.push_back(binaryWriter.tell()); - binaryWriter.write(0); // Temp value, will be replaced later + binaryWriter.write(0xDDDDDDDDu); // Temp value, will be replaced later } // And now we've ready to write node by node @@ -36,23 +38,25 @@ namespace gamelib::loc { if (i > 0) { - // Save current offset + // Save replacement uint32_t newOffset = binaryWriter.tell(); - - // Seek back to origin - ZBioSeekGuard guard { &binaryWriter }; - binaryWriter.seek(offsets[i - 1]); - - // Update offset - binaryWriter.write(newOffset - baseOffset); // Offset calculated from node #0 + replacement.emplace_back(offsets[i], newOffset - baseOffset); } // Write node itself - LOCTreeNode::serialize(children[i], &binaryWriter); + LOCTreeNode::serialize(children[i], &binaryWriter, replacement); } - // Done + // Take value auto raw = binaryWriter.release().value(); + + // Apply replacements + for (const auto& [offset, value] : replacement) + { + *reinterpret_cast(raw.data() + offset) = value; + } + + // Done std::copy(raw.begin(), raw.end(), std::back_inserter(outBuffer)); } } \ No newline at end of file