149 lines
6.6 KiB
C++
149 lines
6.6 KiB
C++
// 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 <array>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
using Item = CItemData::TItemTable;
|
|
using Mob = CPythonNonPlayer::TMobTable;
|
|
|
|
constexpr std::array<DWORD, 4> item_key{{173217, 72619434, 408587239, 27973291}};
|
|
constexpr std::array<DWORD, 4> mob_key{{4813894, 18955, 552631, 6822045}};
|
|
|
|
std::vector<BYTE> 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<char>(stream), std::istreambuf_iterator<char>()};
|
|
}
|
|
|
|
void write_file(const char* path, const std::vector<BYTE>& bytes) {
|
|
std::ofstream stream(path, std::ios::binary | std::ios::trunc);
|
|
if (!stream || !stream.write(reinterpret_cast<const char*>(bytes.data()), bytes.size()))
|
|
throw std::runtime_error(std::string("cannot write ") + path);
|
|
}
|
|
|
|
DWORD word(const std::vector<BYTE>& 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<BYTE>& bytes, std::size_t offset, DWORD value) {
|
|
std::memcpy(bytes.data() + offset, &value, 4);
|
|
}
|
|
|
|
std::map<DWORD, std::string> 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<DWORD, std::string> 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<DWORD>(value)] = name;
|
|
} catch (const std::exception&) { /* header or malformed row */ }
|
|
}
|
|
return names;
|
|
}
|
|
|
|
template <class Record>
|
|
std::vector<BYTE> patch(const char* source, const char* names_path,
|
|
std::array<DWORD, 4> 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<BYTE> 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<BYTE> 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<Item>(argv[1], argv[3], item_key, "MIPX", true);
|
|
const auto mob = patch<Mob>(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;
|
|
}
|
|
}
|