Files
mtgodot-poc/extension/src/proto/proto.cpp
T
shenleiandClaude Opus 5 232d0461be port 2D step 2: 40250 item_proto/mob_proto through CLZO and pack://locale/<lang>/
proto.cpp reads the 40250 MIPX/MMPT tables with the ported CLZO/TEA (156-byte
TItemTable, 255-byte SMobTable), replacing the m2dev XChaCha20 236/335 reader.
Names follow CItemData::GetName / GetMonsterName (szLocaleName, cp1252).
GDScript callers load AssetRoot.locale_file() = pack://locale/en/<name>, which
makes the EterPack chain runtime-reachable; port-map entries for the functions
a coverage run hit move to PORTED/ADAPTED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:34:08 +09:00

254 lines
6.6 KiB
C++

#include "proto.h"
#include "EterBase/StdAfx.h"
#include "EterBase/lzo.h"
#include "GameLib/StdAfx.h"
#include "GameLib/ItemData.h"
#include "../platform/UserInterface/UserInterface.h"
#include <cstring>
#include <fstream>
namespace mtproto {
namespace {
// CPythonNonPlayer::SMobTable (UserInterface/PythonNonPlayer.h), the 40250 ILP32 layout. The mirror of that header
// needs UserInterface/StdAfx.h, which includes the Python headers (after batch 2P); until then the table is here.
#pragma pack(push, 1)
struct SMobSkillLevel {
DWORD dwVnum;
BYTE bLevel;
};
struct SMobTable {
DWORD dwVnum;
char szName[24 + 1]; // CHARACTER_NAME_MAX_LEN (UserInterface/StdAfx.h)
char szLocaleName[24 + 1];
BYTE bType;
BYTE bRank;
BYTE bBattleType;
BYTE bLevel;
BYTE bSize;
DWORD dwGoldMin;
DWORD dwGoldMax;
DWORD dwExp;
DWORD dwMaxHP;
BYTE bRegenCycle;
BYTE bRegenPercent;
WORD wDef;
DWORD dwAIFlag;
DWORD dwRaceFlag;
DWORD dwImmuneFlag;
BYTE bStr, bDex, bCon, bInt;
DWORD dwDamageRange[2];
short sAttackSpeed;
short sMovingSpeed;
BYTE bAggresiveHPPct;
WORD wAggressiveSight;
WORD wAttackRange;
char cEnchants[6]; // MOB_ENCHANTS_MAX_NUM
char cResists[11]; // MOB_RESISTS_MAX_NUM
DWORD dwResurrectionVnum;
DWORD dwDropItemVnum;
BYTE bMountCapacity;
BYTE bOnClickType;
BYTE bEmpire;
char szFolder[64 + 1];
float fDamMultiply;
DWORD dwSummonVnum;
DWORD dwDrainSP;
DWORD dwMonsterColor;
DWORD dwPolymorphItemVnum;
SMobSkillLevel Skills[5]; // MOB_SKILL_MAX_NUM
BYTE bBerserkPoint;
BYTE bStoneSkinPoint;
BYTE bGodSpeedPoint;
BYTE bDeathBlowPoint;
BYTE bRevivePoint;
};
#pragma pack(pop)
static_assert(sizeof(SMobTable) == 255, "mob_proto record");
using TItemTable = CItemData::TItemTable;
uint32_t rd_u32(const uint8_t *p) {
uint32_t v;
std::memcpy(&v, p, 4);
return v;
}
std::string cstr(const char *p, size_t maxn) {
size_t n = 0;
while (n < maxn && p[n]) {
++n;
}
return std::string(p, n);
}
} // namespace
bool load_proto(const std::string &path, const std::array<uint32_t, 4> &key, Proto &out,
std::string *err) {
std::ifstream f(path, std::ios::binary);
if (!f) {
if (err) *err = "cannot open " + path;
return false;
}
std::vector<uint8_t> file((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
return load_proto_bytes(file, key, out, err);
}
// The header checks of CItemManager::LoadItemTable / CPythonNonPlayer::LoadNonPlayerData, then CLZO::Decompress.
bool load_proto_bytes(const std::vector<uint8_t> &file, const std::array<uint32_t, 4> &key,
Proto &out, std::string *err) {
out = Proto{};
if (file.size() < 12) {
if (err) *err = "file too small";
return false;
}
size_t pos = 0;
auto read_u32 = [&](uint32_t &v) {
if (pos + 4 > file.size()) {
return false;
}
v = rd_u32(file.data() + pos);
pos += 4;
return true;
};
read_u32(out.fourcc);
if (out.fourcc == MAKEFOURCC('M', 'I', 'P', 'X')) {
if (!read_u32(out.version) || !read_u32(out.stride)) {
if (err) *err = "truncated MIPX header";
return false;
}
if (out.version != 1) {
if (err) *err = "invalid item_proto VERSION " + std::to_string(out.version);
return false;
}
if (out.stride != sizeof(TItemTable)) {
if (err) *err = "invalid item_proto STRIDE " + std::to_string(out.stride) + " != sizeof(SItemTable)";
return false;
}
} else if (out.fourcc == MAKEFOURCC('M', 'I', 'P', 'T')) {
out.stride = sizeof(TItemTable);
} else if (out.fourcc == MAKEFOURCC('M', 'M', 'P', 'T')) {
out.stride = sizeof(SMobTable);
} else {
if (err) *err = "invalid proto type";
return false;
}
uint32_t data_size = 0;
if (!read_u32(out.elements) || !read_u32(data_size)) {
if (err) *err = "truncated proto header";
return false;
}
// 40250 reads dwDataSize bytes without checking; CLZO then reads its own header lengths.
if (pos + data_size > file.size() || data_size < sizeof(CLZObject::THeader)) {
if (err) *err = "declared data_size past end of file";
return false;
}
PackSingletons(); // CLZO, as 40250 Main() creates it
DWORD dwKey[4] = {key[0], key[1], key[2], key[3]};
CLZObject zObj;
if (!CLZO::Instance().Decompress(zObj, file.data() + pos, dwKey)) {
if (err) *err = "CLZO::Decompress failed (wrong key or corrupt data)";
return false;
}
if (out.fourcc == MAKEFOURCC('M', 'M', 'P', 'T') && (zObj.GetSize() % sizeof(SMobTable)) != 0) {
if (err) *err = "invalid mob_proto size " + std::to_string(zObj.GetSize()) + ", check data format";
return false;
}
// 40250 walks dwElements records from the buffer without comparing against its size.
if (static_cast<uint64_t>(out.stride) * out.elements > zObj.GetSize()) {
if (err) *err = "elements * stride past decompressed size";
return false;
}
const BYTE *buf = zObj.GetBuffer();
out.blob.assign(buf, buf + static_cast<size_t>(out.stride) * out.elements);
return true;
}
ItemRecord parse_item(const uint8_t *r, uint32_t stride) {
ItemRecord it;
if (!r || stride != sizeof(TItemTable)) {
return it;
}
TItemTable t;
std::memcpy(&t, r, sizeof(t));
it.vnum = t.dwVnum;
it.vnum_range = t.dwVnumRange;
it.name = cstr(t.szName, sizeof(t.szName));
it.locale_name = cstr(t.szLocaleName, sizeof(t.szLocaleName));
it.type = t.bType;
it.sub_type = t.bSubType;
it.weight = t.bWeight;
it.size = t.bSize;
it.anti_flags = t.dwAntiFlags;
it.flags = t.dwFlags;
it.wear_flags = t.dwWearFlags;
it.immune_flag = t.dwImmuneFlag;
it.buy_price = t.dwIBuyItemPrice;
it.sell_price = t.dwISellItemPrice;
for (int i = 0; i < CItemData::ITEM_LIMIT_MAX_NUM; ++i) {
it.limits[i] = {t.aLimits[i].bType, t.aLimits[i].lValue};
}
for (int i = 0; i < CItemData::ITEM_APPLY_MAX_NUM; ++i) {
it.applies[i] = {t.aApplies[i].bType, t.aApplies[i].lValue};
}
for (int i = 0; i < CItemData::ITEM_VALUES_MAX_NUM; ++i) {
it.values[i] = t.alValues[i];
}
for (int i = 0; i < CItemData::ITEM_SOCKET_MAX_NUM; ++i) {
it.sockets[i] = t.alSockets[i];
}
it.refined_vnum = t.dwRefinedVnum;
it.refine_set = t.wRefineSet;
it.alter_to_magic_pct = t.bAlterToMagicItemPct;
it.specular = t.bSpecular;
it.gain_socket_pct = t.bGainSocketPct;
return it;
}
MobRecord parse_mob(const uint8_t *r, uint32_t stride) {
MobRecord m;
if (!r || stride != sizeof(SMobTable)) {
return m;
}
SMobTable t;
std::memcpy(&t, r, sizeof(t));
m.vnum = t.dwVnum;
m.name = cstr(t.szName, sizeof(t.szName));
m.locale_name = cstr(t.szLocaleName, sizeof(t.szLocaleName));
m.type = t.bType;
m.rank = t.bRank;
m.battle_type = t.bBattleType;
m.level = t.bLevel;
m.size = t.bSize;
m.attack_speed = t.sAttackSpeed;
m.moving_speed = t.sMovingSpeed;
m.on_click_type = t.bOnClickType;
m.empire = t.bEmpire;
m.folder = cstr(t.szFolder, sizeof(t.szFolder));
m.monster_color = t.dwMonsterColor;
return m;
}
} // namespace mtproto