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>
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
// Platform-layer pieces of 40250 UserInterface/UserInterface.cpp (the WinMain unit). 40250 declares these nowhere:
|
||||
// Main() in the same file calls them. The extension's pack bootstrap calls them instead.
|
||||
|
||||
// 40250: PackInitialize. Registers <folder>/Index the way the 40250 client does at startup. The CLZO and
|
||||
// CEterPackManager singletons it needs must already exist (see PackSingletons).
|
||||
// 40250: PackInitialize
|
||||
// Registers <folder>/Index the way the 40250 client does at startup. The CLZO and CEterPackManager singletons
|
||||
// it needs must already exist (see PackSingletons).
|
||||
bool PackInitialize(const char * c_pszFolder);
|
||||
|
||||
// The two function-static singletons 40250 Main() creates before PackInitialize("pack")
|
||||
|
||||
@@ -369,13 +369,13 @@ class CItemData
|
||||
typedef struct SItemLimit
|
||||
{
|
||||
BYTE bType;
|
||||
long lValue;
|
||||
LONG lValue; // PORT: fixed-width Win32 long (item_proto record layout)
|
||||
} TItemLimit;
|
||||
|
||||
typedef struct SItemApply
|
||||
{
|
||||
BYTE bType;
|
||||
long lValue;
|
||||
LONG lValue; // PORT: fixed-width Win32 long (item_proto record layout)
|
||||
} TItemApply;
|
||||
|
||||
typedef struct SItemTable
|
||||
@@ -400,14 +400,15 @@ class CItemData
|
||||
|
||||
TItemLimit aLimits[ITEM_LIMIT_MAX_NUM];
|
||||
TItemApply aApplies[ITEM_APPLY_MAX_NUM];
|
||||
long alValues[ITEM_VALUES_MAX_NUM];
|
||||
long alSockets[ITEM_SOCKET_MAX_NUM];
|
||||
LONG alValues[ITEM_VALUES_MAX_NUM]; // PORT: fixed-width Win32 long
|
||||
LONG alSockets[ITEM_SOCKET_MAX_NUM]; // PORT: fixed-width Win32 long
|
||||
DWORD dwRefinedVnum;
|
||||
WORD wRefineSet;
|
||||
BYTE bAlterToMagicItemPct;
|
||||
BYTE bSpecular;
|
||||
BYTE bGainSocketPct;
|
||||
} TItemTable;
|
||||
static_assert(sizeof(SItemTable) == 156, "item_proto MIPX stride"); // PORT: 40250 ILP32 layout
|
||||
|
||||
// typedef struct SItemTable
|
||||
// {
|
||||
|
||||
+172
-164
@@ -1,22 +1,87 @@
|
||||
#include "proto.h"
|
||||
|
||||
#include <sodium.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>
|
||||
|
||||
extern "C" {
|
||||
#include <lzo/lzo1x.h>
|
||||
}
|
||||
|
||||
namespace mtproto {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t FOURCC_MIPX = 0x5850494D; // "MIPX" bytes 4D 49 50 58 (LE u32)
|
||||
constexpr uint32_t FOURCC_MIPT = 0x5450494D; // "MIPT"
|
||||
constexpr uint32_t FOURCC_MMPT = 0x54504D4D; // "MMPT"
|
||||
constexpr uint32_t FOURCC_MCOZ = 0x5A4F434D; // "MCOZ"
|
||||
// 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;
|
||||
@@ -24,81 +89,12 @@ uint32_t rd_u32(const uint8_t *p) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// The fork's tea_decrypt (EterBase/tea.cpp): XChaCha20 with key/nonce derived from
|
||||
// the 16-byte input key via BLAKE2b. size is rounded up to a multiple of 8.
|
||||
void tea_decrypt(uint8_t *dst, const uint8_t *src, const std::array<uint32_t, 4> &key32,
|
||||
size_t size) {
|
||||
uint8_t key16[16];
|
||||
std::memcpy(key16, key32.data(), 16); // 4 LE dwords -> raw bytes
|
||||
|
||||
uint8_t dkey[crypto_stream_xchacha20_KEYBYTES];
|
||||
uint8_t nonce[crypto_stream_xchacha20_NONCEBYTES];
|
||||
crypto_generichash(dkey, sizeof(dkey), key16, 16,
|
||||
reinterpret_cast<const uint8_t *>("M2DevPackEncrypt"), 16);
|
||||
uint8_t nonce_seed[crypto_stream_xchacha20_NONCEBYTES + 8];
|
||||
crypto_generichash(nonce_seed, sizeof(nonce_seed), key16, 16,
|
||||
reinterpret_cast<const uint8_t *>("M2DevNonce"), 10);
|
||||
std::memcpy(nonce, nonce_seed, sizeof(nonce));
|
||||
|
||||
size_t rs = (size % 8 == 0) ? size : size + 8 - (size % 8);
|
||||
crypto_stream_xchacha20_xor(dst, src, rs, nonce, dkey);
|
||||
sodium_memzero(dkey, sizeof(dkey));
|
||||
}
|
||||
|
||||
// CLZO container -> decompressed bytes.
|
||||
bool clzo_decompress(const uint8_t *blob, size_t blob_len, const std::array<uint32_t, 4> &key,
|
||||
std::vector<uint8_t> &out, std::string *err) {
|
||||
if (blob_len < 20 || rd_u32(blob) != FOURCC_MCOZ) {
|
||||
if (err) *err = "CLZO: bad MCOZ header";
|
||||
return false;
|
||||
}
|
||||
const uint32_t enc_size = rd_u32(blob + 4);
|
||||
const uint32_t comp_size = rd_u32(blob + 8);
|
||||
const uint32_t real_size = rd_u32(blob + 12);
|
||||
|
||||
out.assign(real_size, 0);
|
||||
lzo_uint out_len = real_size;
|
||||
int r;
|
||||
|
||||
if (enc_size > 0) {
|
||||
size_t rs = (enc_size % 8 == 0) ? enc_size : enc_size + 8 - (enc_size % 8);
|
||||
if (16 + rs > blob_len) {
|
||||
if (err) *err = "CLZO: encrypted region past end";
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> dec(rs);
|
||||
tea_decrypt(dec.data(), blob + 16, key, enc_size); // src = blob+16 (== m_pbIn-4)
|
||||
if (rd_u32(dec.data()) != FOURCC_MCOZ) {
|
||||
if (err) *err = "CLZO: wrong key (inner MCOZ mismatch)";
|
||||
return false;
|
||||
}
|
||||
r = lzo1x_decompress_safe(dec.data() + 4, comp_size, out.data(), &out_len, nullptr);
|
||||
} else {
|
||||
if (20u + comp_size > blob_len) {
|
||||
if (err) *err = "CLZO: compressed region past end";
|
||||
return false;
|
||||
}
|
||||
r = lzo1x_decompress_safe(blob + 20, comp_size, out.data(), &out_len, nullptr);
|
||||
}
|
||||
|
||||
if (r != LZO_E_OK) {
|
||||
if (err) *err = "CLZO: lzo1x_decompress_safe failed (" + std::to_string(r) + ")";
|
||||
return false;
|
||||
}
|
||||
if (out_len != real_size) {
|
||||
if (err) *err = "CLZO: size mismatch " + std::to_string(out_len) + " != " +
|
||||
std::to_string(real_size);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string cstr(const uint8_t *p, size_t maxn) {
|
||||
std::string cstr(const char *p, size_t maxn) {
|
||||
size_t n = 0;
|
||||
while (n < maxn && p[n]) {
|
||||
++n;
|
||||
}
|
||||
return std::string(reinterpret_cast<const char *>(p), n);
|
||||
return std::string(p, n);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -114,131 +110,143 @@ bool load_proto(const std::string &path, const std::array<uint32_t, 4> &key, Pro
|
||||
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) {
|
||||
if (sodium_init() < 0) {
|
||||
if (err) *err = "sodium_init failed";
|
||||
return false;
|
||||
}
|
||||
if (lzo_init() != LZO_E_OK) {
|
||||
if (err) *err = "lzo_init failed";
|
||||
return false;
|
||||
}
|
||||
if (file.size() < 16) {
|
||||
out = Proto{};
|
||||
if (file.size() < 12) {
|
||||
if (err) *err = "file too small";
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t *p = file.data();
|
||||
out.fourcc = rd_u32(p);
|
||||
p += 4;
|
||||
|
||||
uint32_t data_size = 0;
|
||||
if (out.fourcc == FOURCC_MIPX) {
|
||||
out.version = rd_u32(p);
|
||||
p += 4;
|
||||
out.stride = rd_u32(p);
|
||||
p += 4;
|
||||
out.elements = rd_u32(p);
|
||||
p += 4;
|
||||
data_size = rd_u32(p);
|
||||
p += 4;
|
||||
if (out.version != 1) {
|
||||
if (err) *err = "MIPX version != 1";
|
||||
size_t pos = 0;
|
||||
auto read_u32 = [&](uint32_t &v) {
|
||||
if (pos + 4 > file.size()) {
|
||||
return false;
|
||||
}
|
||||
} else if (out.fourcc == FOURCC_MIPT || out.fourcc == FOURCC_MMPT) {
|
||||
out.elements = rd_u32(p);
|
||||
p += 4;
|
||||
data_size = rd_u32(p);
|
||||
p += 4;
|
||||
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 = "unknown proto fourcc";
|
||||
if (err) *err = "invalid proto type";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (static_cast<size_t>(p - file.data()) + data_size > file.size()) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!clzo_decompress(p, data_size, key, out.blob, err)) {
|
||||
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.elements == 0) {
|
||||
if (err) *err = "0 elements";
|
||||
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;
|
||||
}
|
||||
if (out.stride == 0) {
|
||||
if (out.blob.size() % out.elements != 0) {
|
||||
if (err) *err = "blob not divisible by element count";
|
||||
return false;
|
||||
}
|
||||
out.stride = static_cast<uint32_t>(out.blob.size() / out.elements);
|
||||
}
|
||||
if (static_cast<size_t>(out.stride) * out.elements != out.blob.size()) {
|
||||
if (err) *err = "stride * elements != blob size";
|
||||
// 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 < 236) {
|
||||
if (!r || stride != sizeof(TItemTable)) {
|
||||
return it;
|
||||
}
|
||||
it.vnum = rd_u32(r + 0);
|
||||
it.vnum_range = rd_u32(r + 4);
|
||||
it.name = cstr(r + 8, 65);
|
||||
it.locale_name = cstr(r + 73, 65);
|
||||
it.type = r[138];
|
||||
it.sub_type = r[139];
|
||||
it.weight = r[140];
|
||||
it.size = r[141];
|
||||
it.anti_flags = rd_u32(r + 142);
|
||||
it.flags = rd_u32(r + 146);
|
||||
it.wear_flags = rd_u32(r + 150);
|
||||
it.buy_price = rd_u32(r + 158);
|
||||
it.sell_price = rd_u32(r + 162);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const size_t off = 166 + static_cast<size_t>(i) * 5;
|
||||
it.limits[i].type = r[off];
|
||||
it.limits[i].value = static_cast<int32_t>(rd_u32(r + off + 1));
|
||||
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 < 3; ++i) {
|
||||
const size_t off = 176 + static_cast<size_t>(i) * 5;
|
||||
it.applies[i].type = r[off];
|
||||
it.applies[i].value = static_cast<int32_t>(rd_u32(r + off + 1));
|
||||
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 < 6; ++i) {
|
||||
it.values[i] = (int32_t)rd_u32(r + 191 + i * 4);
|
||||
for (int i = 0; i < CItemData::ITEM_VALUES_MAX_NUM; ++i) {
|
||||
it.values[i] = t.alValues[i];
|
||||
}
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
it.sockets[i] = static_cast<int32_t>(rd_u32(r + 215 + i * 4));
|
||||
for (int i = 0; i < CItemData::ITEM_SOCKET_MAX_NUM; ++i) {
|
||||
it.sockets[i] = t.alSockets[i];
|
||||
}
|
||||
it.refined_vnum = rd_u32(r + 227);
|
||||
it.refine_set = static_cast<uint16_t>(r[231] | (static_cast<uint16_t>(r[232]) << 8));
|
||||
it.alter_to_magic_pct = r[233];
|
||||
it.specular = r[234];
|
||||
it.gain_socket_pct = r[235];
|
||||
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 < 139) {
|
||||
if (!r || stride != sizeof(SMobTable)) {
|
||||
return m;
|
||||
}
|
||||
m.vnum = rd_u32(r + 0);
|
||||
m.name = cstr(r + 4, 65);
|
||||
m.locale_name = cstr(r + 69, 65);
|
||||
m.type = r[134];
|
||||
m.rank = r[135];
|
||||
m.battle_type = r[136];
|
||||
m.level = r[137];
|
||||
m.size = r[138];
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+52
-42
@@ -1,15 +1,13 @@
|
||||
#pragma once
|
||||
// item_proto / mob_proto reader for this m2dev fork.
|
||||
// item_proto / mob_proto reader, the 40250 format (GameLib/ItemManager.cpp CItemManager::LoadItemTable,
|
||||
// UserInterface/PythonNonPlayer.cpp CPythonNonPlayer::LoadNonPlayerData).
|
||||
//
|
||||
// Outer: MIPX (item) = [u32 'MIPX'][u32 ver=1][u32 stride][u32 elements][u32 datasize][blob]
|
||||
// MMPT (mob) = [u32 'MMPT'][u32 elements][u32 datasize][blob]
|
||||
// Blob = CLZO container: [u32 'MCOZ'][u32 encryptSize][u32 compressedSize][u32 realSize]
|
||||
// data @ blob+20. encryptSize>0 -> XChaCha20-decrypt (the fork's tea_*: key =
|
||||
// BLAKE2b(key16,"M2DevPackEncrypt"), nonce = BLAKE2b(key16,"M2DevNonce")[:24]) of
|
||||
// encryptSize bytes starting at blob+16, yielding [u32 'MCOZ'][lzo1x] -> LZO ->
|
||||
// realSize bytes. encryptSize==0 -> LZO straight from blob+20.
|
||||
// Decompressed blob = `elements` records of `stride` bytes (stride from the MIPX header,
|
||||
// or realSize/elements for MMPT). Records are #pragma pack(1).
|
||||
// Outer: item = [u32 'MIPX'][u32 ver=1][u32 stride=156][u32 elements][u32 datasize][CLZObject]
|
||||
// (or the older [u32 'MIPT'][u32 elements][u32 datasize][CLZObject])
|
||||
// mob = [u32 'MMPT'][u32 elements][u32 datasize][CLZObject]
|
||||
// CLZObject = [u32 'MCOZ'][u32 encryptSize][u32 compressedSize][u32 realSize] + TEA/LZO payload, decoded by the
|
||||
// ported CLZO::Decompress with the table's key. Records are #pragma pack(1) ILP32 structs:
|
||||
// CItemData::TItemTable (156 bytes, port/GameLib/ItemData.h) and CPythonNonPlayer::TMobTable (255 bytes).
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -18,14 +16,15 @@
|
||||
|
||||
namespace mtproto {
|
||||
|
||||
// The 4-DWORD keys the client hardcodes (GameLib/ItemManager.cpp, PythonNonPlayer.cpp).
|
||||
// The 4-DWORD keys 40250 hardcodes: s_adwItemProtoKey (GameLib/ItemManager.cpp), s_adwMobProtoKey
|
||||
// (UserInterface/PythonNonPlayer.cpp).
|
||||
inline constexpr std::array<uint32_t, 4> ITEM_PROTO_KEY = {173217u, 72619434u, 408587239u, 27973291u};
|
||||
inline constexpr std::array<uint32_t, 4> MOB_PROTO_KEY = {4813894u, 18955u, 552631u, 6822045u};
|
||||
|
||||
struct Proto {
|
||||
uint32_t fourcc = 0;
|
||||
uint32_t version = 0;
|
||||
uint32_t stride = 0; // record size
|
||||
uint32_t stride = 0; // record size: sizeof(TItemTable) or sizeof(TMobTable)
|
||||
uint32_t elements = 0; // record count
|
||||
std::vector<uint8_t> blob; // elements * stride bytes
|
||||
|
||||
@@ -34,14 +33,14 @@ struct Proto {
|
||||
}
|
||||
};
|
||||
|
||||
// Load + decompress. `key` is ITEM_PROTO_KEY or MOB_PROTO_KEY.
|
||||
// Parse from an already-read buffer (host reads via godot::FileAccess for res://).
|
||||
// Load + decompress. `key` is ITEM_PROTO_KEY or MOB_PROTO_KEY; the format follows the fourcc.
|
||||
// Parse from an already-read buffer (the extension reads through asset_io, e.g. pack://locale/en/item_proto).
|
||||
bool load_proto_bytes(const std::vector<uint8_t> &bytes, const std::array<uint32_t, 4> &key,
|
||||
Proto &out, std::string *err);
|
||||
bool load_proto(const std::string &path, const std::array<uint32_t, 4> &key, Proto &out,
|
||||
std::string *err = nullptr);
|
||||
|
||||
// --- typed views over the leading fields (rest is offset-stable per stride) ---
|
||||
// --- typed views over the records ---
|
||||
|
||||
struct ItemLimit {
|
||||
uint8_t type = 0;
|
||||
@@ -53,42 +52,53 @@ struct ItemApply {
|
||||
int32_t value = 0;
|
||||
};
|
||||
|
||||
// CItemData::TItemTable, field for field.
|
||||
struct ItemRecord {
|
||||
uint32_t vnum = 0;
|
||||
uint32_t vnum_range = 0;
|
||||
std::string name; // szName[65] @ 8
|
||||
std::string locale_name; // szLocaleName[65] @ 73
|
||||
uint8_t type = 0; // @ 138
|
||||
uint8_t sub_type = 0; // @ 139
|
||||
uint8_t weight = 0; // @ 140
|
||||
uint8_t size = 0; // @ 141
|
||||
uint32_t anti_flags = 0; // @ 142 (ITEM_ANTIFLAG_*; sell = 1 << 8)
|
||||
uint32_t flags = 0; // @ 146 (ITEM_FLAG_*; count-per-1-gold = 1 << 3)
|
||||
uint32_t wear_flags = 0; // @ 150
|
||||
uint32_t buy_price = 0; // @ 158
|
||||
uint32_t sell_price = 0; // @ 162
|
||||
std::array<ItemLimit, 2> limits{}; // aLimits[2] @ 166 (type + long)
|
||||
std::array<ItemApply, 3> applies{}; // aApplies[3] @ 176 (type + long)
|
||||
int32_t values[6] = {0}; // alValues[6] @ 191 (armor: values[3] = body shape index)
|
||||
int32_t sockets[3] = {0}; // alSockets[3] @ 215
|
||||
uint32_t refined_vnum = 0; // dwRefinedVnum @ 227
|
||||
uint16_t refine_set = 0; // wRefineSet @ 231
|
||||
uint8_t alter_to_magic_pct = 0; // bAlterToMagicItemPct @ 233
|
||||
uint8_t specular = 0; // @ 234 -> PARITY §2.7 fSpecular = specular/100
|
||||
uint8_t gain_socket_pct = 0; // bGainSocketPct @ 235
|
||||
std::string name; // szName[25]
|
||||
std::string locale_name; // szLocaleName[25]
|
||||
uint8_t type = 0;
|
||||
uint8_t sub_type = 0;
|
||||
uint8_t weight = 0;
|
||||
uint8_t size = 0;
|
||||
uint32_t anti_flags = 0; // ITEM_ANTIFLAG_*
|
||||
uint32_t flags = 0; // ITEM_FLAG_*
|
||||
uint32_t wear_flags = 0;
|
||||
uint32_t immune_flag = 0;
|
||||
uint32_t buy_price = 0; // dwIBuyItemPrice
|
||||
uint32_t sell_price = 0; // dwISellItemPrice
|
||||
std::array<ItemLimit, 2> limits{};
|
||||
std::array<ItemApply, 3> applies{};
|
||||
int32_t values[6] = {0}; // alValues (armor: values[3] = body shape index)
|
||||
int32_t sockets[3] = {0};
|
||||
uint32_t refined_vnum = 0;
|
||||
uint16_t refine_set = 0;
|
||||
uint8_t alter_to_magic_pct = 0;
|
||||
uint8_t specular = 0; // PARITY §2.7 fSpecular = specular/100
|
||||
uint8_t gain_socket_pct = 0;
|
||||
};
|
||||
// Empty record when `stride` is not sizeof(TItemTable).
|
||||
ItemRecord parse_item(const uint8_t *rec, uint32_t stride);
|
||||
|
||||
// The leading CPythonNonPlayer::TMobTable fields plus the ones the client reads for display.
|
||||
struct MobRecord {
|
||||
uint32_t vnum = 0;
|
||||
std::string name; // szName[65] @ 4
|
||||
std::string locale_name; // @ 69
|
||||
uint8_t type = 0; // @ 134
|
||||
uint8_t rank = 0; // @ 135
|
||||
uint8_t battle_type = 0; // @ 136
|
||||
uint8_t level = 0; // @ 137
|
||||
uint8_t size = 0; // @ 138
|
||||
std::string name; // szName[25]
|
||||
std::string locale_name; // szLocaleName[25]
|
||||
uint8_t type = 0;
|
||||
uint8_t rank = 0;
|
||||
uint8_t battle_type = 0;
|
||||
uint8_t level = 0;
|
||||
uint8_t size = 0;
|
||||
int16_t attack_speed = 0;
|
||||
int16_t moving_speed = 0;
|
||||
uint8_t on_click_type = 0;
|
||||
uint8_t empire = 0;
|
||||
std::string folder; // szFolder[65]
|
||||
uint32_t monster_color = 0;
|
||||
};
|
||||
// Empty record when `stride` is not sizeof(TMobTable).
|
||||
MobRecord parse_mob(const uint8_t *rec, uint32_t stride);
|
||||
|
||||
} // namespace mtproto
|
||||
|
||||
@@ -10,6 +10,24 @@ using namespace godot;
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
namespace {
|
||||
|
||||
// szName is the Korean (CP949) development name; what 40250 shows is szLocaleName (CItemData::GetName,
|
||||
// CPythonNonPlayer::GetMonsterName) in the locale code page, 1252 for locale.cfg "10002 1252 en".
|
||||
String from_cp1252(const std::string &s) {
|
||||
static const char32_t k80[32] = {
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFD, 0x017D, 0xFFFD,
|
||||
0xFFFD, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFD, 0x017E, 0x0178,
|
||||
};
|
||||
String out;
|
||||
for (unsigned char c : s) {
|
||||
out += String::chr(c >= 0x80 && c < 0xA0 ? k80[c - 0x80] : static_cast<char32_t>(c));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Metin2Proto::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("load_item_proto", "path"), &Metin2Proto::load_item_proto);
|
||||
ClassDB::bind_method(D_METHOD("load_mob_proto", "path"), &Metin2Proto::load_mob_proto);
|
||||
@@ -65,8 +83,8 @@ Dictionary Metin2Proto::item(int vnum) const {
|
||||
mtproto::ItemRecord r = mtproto::parse_item(m_item.record(it->second), m_item.stride);
|
||||
d["vnum"] = (int)r.vnum;
|
||||
d["vnum_range"] = (int)r.vnum_range;
|
||||
d["name"] = String::utf8(r.name.c_str());
|
||||
d["locale_name"] = String::utf8(r.locale_name.c_str());
|
||||
d["name"] = from_cp1252(r.locale_name); // CItemData::GetName()
|
||||
d["locale_name"] = from_cp1252(r.locale_name);
|
||||
d["type"] = (int)r.type;
|
||||
d["sub_type"] = (int)r.sub_type;
|
||||
d["weight"] = (int)r.weight;
|
||||
@@ -74,6 +92,7 @@ Dictionary Metin2Proto::item(int vnum) const {
|
||||
d["anti_flags"] = (int)r.anti_flags;
|
||||
d["flags"] = (int)r.flags;
|
||||
d["wear_flags"] = (int)r.wear_flags;
|
||||
d["immune_flag"] = (int)r.immune_flag;
|
||||
d["buy_price"] = (int)r.buy_price;
|
||||
d["sell_price"] = (int)r.sell_price;
|
||||
{
|
||||
@@ -126,13 +145,19 @@ Dictionary Metin2Proto::mob(int vnum) const {
|
||||
}
|
||||
mtproto::MobRecord r = mtproto::parse_mob(m_mob.record(it->second), m_mob.stride);
|
||||
d["vnum"] = (int)r.vnum;
|
||||
d["name"] = String::utf8(r.name.c_str());
|
||||
d["locale_name"] = String::utf8(r.locale_name.c_str());
|
||||
d["name"] = from_cp1252(r.locale_name); // CPythonNonPlayer::GetMonsterName()
|
||||
d["locale_name"] = from_cp1252(r.locale_name);
|
||||
d["type"] = (int)r.type;
|
||||
d["rank"] = (int)r.rank;
|
||||
d["battle_type"] = (int)r.battle_type;
|
||||
d["level"] = (int)r.level;
|
||||
d["size"] = (int)r.size;
|
||||
d["attack_speed"] = (int)r.attack_speed;
|
||||
d["moving_speed"] = (int)r.moving_speed;
|
||||
d["on_click_type"] = (int)r.on_click_type;
|
||||
d["empire"] = (int)r.empire;
|
||||
d["folder"] = String(r.folder.c_str());
|
||||
d["monster_color"] = (int64_t)r.monster_color;
|
||||
return d;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Metin2Proto — GDExtension node exposing item_proto / mob_proto to GDScript.
|
||||
//
|
||||
// var proto = Metin2Proto.new()
|
||||
// proto.load_item_proto("<assets>/locale/locale/en/item_proto")
|
||||
// proto.load_item_proto("pack://locale/en/item_proto") # 40250 format, via asset_io
|
||||
// var d := proto.item(19) # { vnum, name, locale_name, type, sub_type, ... }
|
||||
//
|
||||
// Used by the P2 inventory/equipment windows for names / types / tooltips and by
|
||||
|
||||
Reference in New Issue
Block a user