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:
@@ -1,5 +1,5 @@
|
||||
// Synthetic item_proto record test. It locks the offsets used by the
|
||||
// ClientVS22 TItemTable layout without requiring the 2.2G asset tree.
|
||||
// Synthetic item_proto / mob_proto records. Locks the 40250 CItemData::TItemTable (156 bytes) and
|
||||
// CPythonNonPlayer::TMobTable (255 bytes) offsets without the client assets.
|
||||
#include "../src/proto/proto.h"
|
||||
|
||||
#include <array>
|
||||
@@ -16,51 +16,59 @@ static int g_fail = 0;
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void put_u32(std::array<uint8_t, 236> &record, size_t offset, uint32_t value) {
|
||||
template <size_t N>
|
||||
static void put_u32(std::array<uint8_t, N> &record, size_t offset, uint32_t value) {
|
||||
record[offset + 0] = static_cast<uint8_t>(value);
|
||||
record[offset + 1] = static_cast<uint8_t>(value >> 8);
|
||||
record[offset + 2] = static_cast<uint8_t>(value >> 16);
|
||||
record[offset + 3] = static_cast<uint8_t>(value >> 24);
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::array<uint8_t, 236> record{};
|
||||
template <size_t N>
|
||||
static void put_u16(std::array<uint8_t, N> &record, size_t offset, uint16_t value) {
|
||||
record[offset + 0] = static_cast<uint8_t>(value);
|
||||
record[offset + 1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
static void item_layout() {
|
||||
std::array<uint8_t, 156> record{};
|
||||
put_u32(record, 0, 12345);
|
||||
put_u32(record, 4, 7);
|
||||
std::memcpy(record.data() + 8, "TestItem", 9);
|
||||
std::memcpy(record.data() + 73, "LocaleItem", 11);
|
||||
record[138] = 3;
|
||||
record[139] = 4;
|
||||
record[140] = 2;
|
||||
record[141] = 1;
|
||||
put_u32(record, 142, 1u << 8);
|
||||
put_u32(record, 146, 1u << 3);
|
||||
put_u32(record, 150, 0x12345678);
|
||||
put_u32(record, 158, 9000);
|
||||
put_u32(record, 162, 6000);
|
||||
record[166] = 1;
|
||||
put_u32(record, 167, 42);
|
||||
record[176] = 2;
|
||||
put_u32(record, 177, -7);
|
||||
put_u32(record, 191 + 3 * 4, 3);
|
||||
put_u32(record, 215, 1001);
|
||||
put_u32(record, 219, -1);
|
||||
put_u32(record, 227, 12346);
|
||||
record[231] = 0x34;
|
||||
record[232] = 0x12;
|
||||
record[233] = 11;
|
||||
record[234] = 75;
|
||||
record[235] = 88;
|
||||
std::memcpy(record.data() + 33, "LocaleItem", 11);
|
||||
record[58] = 3;
|
||||
record[59] = 4;
|
||||
record[60] = 2;
|
||||
record[61] = 1;
|
||||
put_u32(record, 62, 1u << 8);
|
||||
put_u32(record, 66, 1u << 3);
|
||||
put_u32(record, 70, 0x12345678);
|
||||
put_u32(record, 74, 0x55);
|
||||
put_u32(record, 78, 9000);
|
||||
put_u32(record, 82, 6000);
|
||||
record[86] = 1;
|
||||
put_u32(record, 87, 42);
|
||||
record[96] = 2;
|
||||
put_u32(record, 97, static_cast<uint32_t>(-7));
|
||||
put_u32(record, 111 + 3 * 4, 3);
|
||||
put_u32(record, 135, 1001);
|
||||
put_u32(record, 139, static_cast<uint32_t>(-1));
|
||||
put_u32(record, 147, 12346);
|
||||
put_u16(record, 151, 0x1234);
|
||||
record[153] = 11;
|
||||
record[154] = 75;
|
||||
record[155] = 88;
|
||||
|
||||
const mtproto::ItemRecord item = mtproto::parse_item(record.data(), record.size());
|
||||
CHECK(item.vnum == 12345, "vnum offset");
|
||||
CHECK(item.vnum_range == 7, "vnum range offset");
|
||||
CHECK(item.name == "TestItem", "name offset");
|
||||
CHECK(item.locale_name == "LocaleItem", "locale name offset");
|
||||
CHECK(item.type == 3 && item.sub_type == 4, "type offsets");
|
||||
CHECK(item.type == 3 && item.sub_type == 4 && item.weight == 2 && item.size == 1, "type offsets");
|
||||
CHECK(item.anti_flags == (1u << 8), "anti_flags offset");
|
||||
CHECK(item.flags == (1u << 3), "flags offset");
|
||||
CHECK(item.wear_flags == 0x12345678, "wear_flags offset");
|
||||
CHECK(item.immune_flag == 0x55, "immune_flag offset");
|
||||
CHECK(item.buy_price == 9000 && item.sell_price == 6000, "price offsets");
|
||||
CHECK(item.limits[0].type == 1 && item.limits[0].value == 42, "limit offsets");
|
||||
CHECK(item.applies[0].type == 2 && item.applies[0].value == -7, "apply offsets");
|
||||
@@ -69,11 +77,50 @@ int main() {
|
||||
CHECK(item.refined_vnum == 12346 && item.refine_set == 0x1234, "refine offsets");
|
||||
CHECK(item.alter_to_magic_pct == 11 && item.specular == 75 && item.gain_socket_pct == 88,
|
||||
"tail offsets");
|
||||
CHECK(mtproto::parse_item(record.data(), 236).vnum == 0, "m2dev 236-byte stride rejected");
|
||||
}
|
||||
|
||||
static void mob_layout() {
|
||||
std::array<uint8_t, 255> record{};
|
||||
put_u32(record, 0, 101);
|
||||
std::memcpy(record.data() + 4, "Wild Dog", 9);
|
||||
std::memcpy(record.data() + 29, "LocaleDog", 10);
|
||||
record[54] = 0; // bType
|
||||
record[55] = 1; // bRank
|
||||
record[56] = 2; // bBattleType
|
||||
record[57] = 45; // bLevel
|
||||
record[58] = 1; // bSize
|
||||
// 59 gold/exp/hp (16), 75 regen (2), 77 wDef, 79 AI/race/immune (12), 91 str..int (4), 95 damage (8)
|
||||
put_u16(record, 103, static_cast<uint16_t>(-5)); // sAttackSpeed
|
||||
put_u16(record, 105, 120); // sMovingSpeed
|
||||
// 107 aggr pct, 108 sight, 110 range, 112 enchants[6], 118 resists[11], 129 resurrection, 133 drop
|
||||
record[137] = 3; // bMountCapacity
|
||||
record[138] = 1; // bOnClickType
|
||||
record[139] = 2; // bEmpire
|
||||
std::memcpy(record.data() + 140, "monster/dog", 12);
|
||||
// 205 fDamMultiply, 209 summon, 213 drain sp
|
||||
put_u32(record, 217, 0xAABBCCDD); // dwMonsterColor
|
||||
// 221 polymorph, 225 skills 5x5, 250 berserk..revive
|
||||
record[254] = 9;
|
||||
|
||||
const mtproto::MobRecord m = mtproto::parse_mob(record.data(), record.size());
|
||||
CHECK(m.vnum == 101, "mob vnum offset");
|
||||
CHECK(m.name == "Wild Dog" && m.locale_name == "LocaleDog", "mob name offsets");
|
||||
CHECK(m.rank == 1 && m.battle_type == 2 && m.level == 45 && m.size == 1, "mob byte offsets");
|
||||
CHECK(m.attack_speed == -5 && m.moving_speed == 120, "mob speed offsets");
|
||||
CHECK(m.on_click_type == 1 && m.empire == 2, "mob click/empire offsets");
|
||||
CHECK(m.folder == "monster/dog", "mob folder offset");
|
||||
CHECK(m.monster_color == 0xAABBCCDD, "mob color offset");
|
||||
CHECK(mtproto::parse_mob(record.data(), 335).vnum == 0, "m2dev 335-byte stride rejected");
|
||||
}
|
||||
|
||||
int main() {
|
||||
item_layout();
|
||||
mob_layout();
|
||||
if (g_fail) {
|
||||
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
|
||||
return 1;
|
||||
}
|
||||
std::printf("all item layout checks passed\n");
|
||||
std::printf("all item/mob layout checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// item_proto / mob_proto reader — against the real locale files if present.
|
||||
// Set M2_ASSETS to the Metin2 assets dir, else falls back to a repo-relative
|
||||
// guess; skips (passes) if the files are not found.
|
||||
// item_proto / mob_proto against the real 40250 client: registers <Client>/pack like the client does, reads
|
||||
// "locale/en/item_proto" and "locale/en/mob_proto" through CEterPackManager::Get (the path
|
||||
// CPythonApplication::Create hands LoadItemTable/LoadNonPlayerData), and parses them in the 40250 format.
|
||||
//
|
||||
// proto_test <40250 Client dir>
|
||||
//
|
||||
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which turns that into a failure.
|
||||
#include "../src/proto/proto.h"
|
||||
#include "../src/platform/PackBackend.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -18,46 +23,43 @@ static int g_fail = 0;
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static std::string assets_root() {
|
||||
if (const char *e = std::getenv("M2_ASSETS")) {
|
||||
return e;
|
||||
static bool load(const char *name, const std::array<uint32_t, 4> &key, Proto &p) {
|
||||
std::vector<uint8_t> bytes;
|
||||
if (!mtpack40250::read(name, bytes)) {
|
||||
std::fprintf(stderr, "FAIL: %s not in the packs\n", name);
|
||||
++g_fail;
|
||||
return false;
|
||||
}
|
||||
return "../../assets"; // ctest cwd = build/extension -> <repo>/assets
|
||||
std::string err;
|
||||
const bool ok = load_proto_bytes(bytes, key, p, &err);
|
||||
CHECK(ok, (std::string("load ") + name + ": " + err).c_str());
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool exists(const std::string &p) {
|
||||
FILE *f = std::fopen(p.c_str(), "rb");
|
||||
if (f) {
|
||||
std::fclose(f);
|
||||
return true;
|
||||
int main(int argc, char **argv) {
|
||||
const char *strict = std::getenv("MT_ASSETS_STRICT");
|
||||
const bool is_strict = strict && std::string(strict) == "1";
|
||||
const std::string client = argc > 1 ? argv[1] : "";
|
||||
if (client.empty() || !mtpack40250::initialize(client)) {
|
||||
std::printf("proto_test: no 40250 Client/pack at '%s'\n", client.c_str());
|
||||
return is_strict ? 1 : 77;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int main() {
|
||||
const std::string base = assets_root() + "/locale/locale/en";
|
||||
const std::string ipath = base + "/item_proto";
|
||||
const std::string mpath = base + "/mob_proto";
|
||||
|
||||
if (!exists(ipath) || !exists(mpath)) {
|
||||
std::printf("skipped (no locale proto files at %s)\n", base.c_str());
|
||||
return 0;
|
||||
}
|
||||
// CEterPackManager::isExist, what AssetRoot.locale_file() asks before loading.
|
||||
CHECK(mtpack40250::exists("locale/en/item_proto"), "isExist locale/en/item_proto");
|
||||
CHECK(mtpack40250::exists("LOCALE\\EN\\MOB_PROTO"), "isExist normalizes case and separators");
|
||||
CHECK(!mtpack40250::exists("locale/en/no_such_proto"), "isExist rejects a missing name");
|
||||
|
||||
// --- item_proto ---
|
||||
{
|
||||
Proto p;
|
||||
std::string err;
|
||||
bool ok = load_proto(ipath, ITEM_PROTO_KEY, p, &err);
|
||||
CHECK(ok, ("load item_proto: " + err).c_str());
|
||||
if (ok) {
|
||||
if (load("locale/en/item_proto", ITEM_PROTO_KEY, p)) {
|
||||
CHECK(p.fourcc == 0x5850494Du, "item fourcc MIPX");
|
||||
CHECK(p.version == 1, "item version 1");
|
||||
CHECK(p.stride == 236, "item stride 236 (== pack(1) sizeof TItemTable)");
|
||||
CHECK(p.stride == 156, "item stride 156 (== sizeof TItemTable)");
|
||||
CHECK(p.elements > 100, "item elements > 100");
|
||||
CHECK(p.blob.size() == static_cast<size_t>(p.stride) * p.elements, "item blob size");
|
||||
|
||||
// every record: vnum monotonically increasing, name printable ascii
|
||||
uint32_t prev = 0;
|
||||
int named = 0, mono = 1;
|
||||
uint8_t max_spec = 0;
|
||||
@@ -74,68 +76,61 @@ int main() {
|
||||
max_spec = it.specular;
|
||||
}
|
||||
}
|
||||
CHECK(mono, "item vnums non-decreasing");
|
||||
CHECK(mono, "item vnums increasing");
|
||||
CHECK(named > p.elements * 0.8, "item: >80% have a name");
|
||||
std::printf("item_proto: %u items, stride %u, %d named, max bSpecular=%u\n",
|
||||
p.elements, p.stride, named, max_spec);
|
||||
|
||||
ItemRecord first = parse_item(p.record(0), p.stride);
|
||||
std::printf(" [0] vnum=%u '%s' type=%u sub=%u weight=%u\n", first.vnum,
|
||||
first.name.c_str(), first.type, first.sub_type, first.weight);
|
||||
CHECK(first.vnum > 0 && first.vnum < 100000, "item[0] vnum sane");
|
||||
CHECK(!first.name.empty(), "item[0] has a name");
|
||||
|
||||
// alValues[3] = body-armor shape index (anchored by bSpecular @234).
|
||||
// "Monk Plate Armour" 11200..11209 all share shape 3; specular ramps
|
||||
// with the refine level (0 -> 100).
|
||||
// alValues[3] = body-armor shape index; "Monk Plate Armour" 11200..11209 share shape 3 and
|
||||
// bSpecular ramps with the refine level.
|
||||
bool found_armor = false;
|
||||
for (uint32_t i = 0; i < p.elements; ++i) {
|
||||
ItemRecord it = parse_item(p.record(i), p.stride);
|
||||
if (it.vnum == 11209) {
|
||||
found_armor = true;
|
||||
std::printf(" 11209 values=[%d,%d,%d,%d,%d,%d] spec=%u\n", it.values[0],
|
||||
it.values[1], it.values[2], it.values[3], it.values[4], it.values[5],
|
||||
it.specular);
|
||||
std::printf(" 11209 '%s' values=[%d,%d,%d,%d,%d,%d] spec=%u\n", it.locale_name.c_str(),
|
||||
it.values[0], it.values[1], it.values[2], it.values[3], it.values[4],
|
||||
it.values[5], it.specular);
|
||||
CHECK(it.values[3] == 3, "item 11209: values[3] == shape 3");
|
||||
CHECK(it.specular == 100, "item 11209 (+9): bSpecular == 100");
|
||||
}
|
||||
if (it.vnum == 11200) {
|
||||
CHECK(it.values[3] == 3 && it.specular == 0,
|
||||
"item 11200 (+0): shape 3, specular 0");
|
||||
CHECK(it.values[3] == 3 && it.specular == 0, "item 11200 (+0): shape 3, specular 0");
|
||||
}
|
||||
}
|
||||
CHECK(found_armor, "item_proto contains vnum 11209");
|
||||
// CItemData::GetName() is szLocaleName.
|
||||
ItemRecord yang = parse_item(p.record(0), p.stride);
|
||||
std::printf(" [0] vnum=%u locale_name='%s'\n", yang.vnum, yang.locale_name.c_str());
|
||||
CHECK(yang.vnum == 1 && yang.locale_name == "Yang", "item[0] = vnum 1 'Yang'");
|
||||
}
|
||||
}
|
||||
|
||||
// --- mob_proto ---
|
||||
{
|
||||
Proto p;
|
||||
std::string err;
|
||||
bool ok = load_proto(mpath, MOB_PROTO_KEY, p, &err);
|
||||
CHECK(ok, ("load mob_proto: " + err).c_str());
|
||||
if (ok) {
|
||||
if (load("locale/en/mob_proto", MOB_PROTO_KEY, p)) {
|
||||
CHECK(p.fourcc == 0x54504D4Du, "mob fourcc MMPT");
|
||||
CHECK(p.stride == 335, "mob stride 335 (derived from realSize/elements)");
|
||||
CHECK(p.stride == 255, "mob stride 255 (== sizeof TMobTable)");
|
||||
CHECK(p.elements > 100, "mob elements > 100");
|
||||
|
||||
int named = 0, maxlvl = 0;
|
||||
int named = 0, maxlvl = 0, with_folder = 0;
|
||||
for (uint32_t i = 0; i < p.elements; ++i) {
|
||||
MobRecord m = parse_mob(p.record(i), p.stride);
|
||||
if (!m.name.empty()) {
|
||||
++named;
|
||||
}
|
||||
named += !m.name.empty();
|
||||
with_folder += !m.folder.empty();
|
||||
if (m.level > maxlvl) {
|
||||
maxlvl = m.level;
|
||||
}
|
||||
}
|
||||
CHECK(named > p.elements * 0.8, "mob: >80% named");
|
||||
CHECK(with_folder > p.elements * 0.5, "mob: >50% have a model folder");
|
||||
CHECK(maxlvl > 20 && maxlvl < 256, "mob levels in a sane range");
|
||||
MobRecord first = parse_mob(p.record(0), p.stride);
|
||||
std::printf("mob_proto: %u mobs, stride %u, %d named, maxlvl=%d\n [0] vnum=%u '%s' "
|
||||
"type=%u rank=%u level=%u\n",
|
||||
p.elements, p.stride, named, maxlvl, first.vnum, first.name.c_str(), first.type,
|
||||
first.rank, first.level);
|
||||
"type=%u rank=%u level=%u folder='%s'\n",
|
||||
p.elements, p.stride, named, maxlvl, first.vnum, first.locale_name.c_str(), first.type,
|
||||
first.rank, first.level, first.folder.c_str());
|
||||
CHECK(first.vnum > 0 && first.vnum < 60000, "mob[0] vnum sane");
|
||||
CHECK(!first.name.empty(), "mob[0] has a name");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user