// Offline CP936 name overlay for the 40250 item/mob proto format. // Runtime loading remains the original 40250 path and binary format. #include "GameLib/StdAfx.h" #include "GameLib/ItemData.h" #include "UserInterface/StdAfx.h" #include "UserInterface/PythonNonPlayer.h" #include "EterBase/lzo.h" #include #include #include #include #include #include #include #include #include namespace { using Item = CItemData::TItemTable; using Mob = CPythonNonPlayer::TMobTable; constexpr std::array item_key{{173217, 72619434, 408587239, 27973291}}; constexpr std::array mob_key{{4813894, 18955, 552631, 6822045}}; std::vector read_file(const char* path) { std::ifstream stream(path, std::ios::binary); if (!stream) throw std::runtime_error(std::string("cannot read ") + path); return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; } void write_file(const char* path, const std::vector& bytes) { std::ofstream stream(path, std::ios::binary | std::ios::trunc); if (!stream || !stream.write(reinterpret_cast(bytes.data()), bytes.size())) throw std::runtime_error(std::string("cannot write ") + path); } DWORD word(const std::vector& bytes, std::size_t offset) { if (offset + 4 > bytes.size()) throw std::runtime_error("truncated proto header"); DWORD value; std::memcpy(&value, bytes.data() + offset, 4); return value; } void put_word(std::vector& bytes, std::size_t offset, DWORD value) { std::memcpy(bytes.data() + offset, &value, 4); } std::map read_names(const char* path) { std::ifstream stream(path, std::ios::binary); if (!stream) throw std::runtime_error(std::string("cannot read ") + path); std::map names; std::string line; while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); const std::size_t tab = line.find('\t'); if (tab == std::string::npos || tab == 0) continue; try { std::size_t end = 0; const unsigned long value = std::stoul(line.substr(0, tab), &end); if (end != tab || value > UINT32_MAX) continue; const std::string name = line.substr(tab + 1); if (!name.empty() && name.find('\0') == std::string::npos) names[static_cast(value)] = name; } catch (const std::exception&) { /* header or malformed row */ } } return names; } template std::vector patch(const char* source, const char* names_path, std::array key, const char* magic, bool versioned) { const auto input = read_file(source); const std::size_t count_offset = versioned ? 12 : 4; const std::size_t payload_offset = versioned ? 20 : 12; if (input.size() < payload_offset || std::memcmp(input.data(), magic, 4) != 0) throw std::runtime_error("unexpected proto signature"); if (versioned && (word(input, 4) != 1 || word(input, 8) != sizeof(Record))) throw std::runtime_error("unexpected item proto version or stride"); const DWORD count = word(input, count_offset); const DWORD payload_size = word(input, count_offset + 4); if (payload_size > input.size() - payload_offset) throw std::runtime_error("truncated compressed proto"); CLZObject decoded; if (!CLZO::Instance().Decompress(decoded, input.data() + payload_offset, key.data()) || decoded.GetSize() != std::size_t(count) * sizeof(Record)) throw std::runtime_error("invalid proto payload size or key"); std::vector plain(decoded.GetBuffer(), decoded.GetBuffer() + decoded.GetSize()); const auto original = plain; const auto names = read_names(names_path); std::size_t translated = 0, missing = 0, too_long = 0; for (DWORD i = 0; i < count; ++i) { Record record; std::memcpy(&record, plain.data() + std::size_t(i) * sizeof(Record), sizeof(Record)); const auto found = names.find(record.dwVnum); if (found == names.end()) { ++missing; continue; } if (found->second.size() >= sizeof(record.szLocaleName)) { ++too_long; continue; } std::memset(record.szLocaleName, 0, sizeof(record.szLocaleName)); std::memcpy(record.szLocaleName, found->second.data(), found->second.size()); std::memcpy(plain.data() + std::size_t(i) * sizeof(Record), &record, sizeof(Record)); ++translated; } // No item stats, VNUM, canonical name, or mob gameplay fields may change. for (DWORD i = 0; i < count; ++i) { const std::size_t start = std::size_t(i) * sizeof(Record); const std::size_t name_start = start + offsetof(Record, szLocaleName); const std::size_t name_end = name_start + sizeof(Record::szLocaleName); for (std::size_t byte = start; byte < start + sizeof(Record); ++byte) { if ((byte < name_start || byte >= name_end) && plain[byte] != original[byte]) throw std::runtime_error("non-name proto data changed"); } } CLZObject encoded; if (!CLZO::Instance().CompressEncryptedMemory(encoded, plain.data(), plain.size(), key.data())) throw std::runtime_error("cannot encode proto"); std::vector result(input.begin(), input.begin() + payload_offset); result.insert(result.end(), encoded.GetBuffer(), encoded.GetBuffer() + encoded.GetSize()); put_word(result, count_offset + 4, encoded.GetSize()); CLZObject verified; if (!CLZO::Instance().Decompress(verified, result.data() + payload_offset, key.data()) || verified.GetSize() != plain.size() || std::memcmp(verified.GetBuffer(), plain.data(), plain.size()) != 0) throw std::runtime_error("encoded proto failed round-trip verification"); std::cout << magic << " count=" << count << " translated=" << translated << " missing=" << missing << " too_long=" << too_long << '\n'; return result; } } // namespace int main(int argc, char** argv) { if (argc != 7) { std::cerr << "usage: locale_proto_patch item_proto mob_proto item_names_gbk mob_names_gbk out_item out_mob\n"; return 2; } try { static CLZO lzo; const auto item = patch(argv[1], argv[3], item_key, "MIPX", true); const auto mob = patch(argv[2], argv[4], mob_key, "MMPT", false); write_file(argv[5], item); write_file(argv[6], mob); return 0; } catch (const std::exception& error) { std::cerr << "locale_proto_patch: " << error.what() << '\n'; return 1; } }