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>
254 lines
6.6 KiB
C++
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
|