Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
phases, EntityStore world model, ~all GC/CG headers. char create/delete,
private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
token), system-option + game-option + ESC system menu, private-shop 39-grid,
party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.
Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.
Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).
ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// 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.
|
||||
#include "../src/proto/proto.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
using namespace mtproto;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(c, msg) \
|
||||
do { \
|
||||
if (!(c)) { \
|
||||
std::fprintf(stderr, "FAIL: %s\n", msg); \
|
||||
++g_fail; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static std::string assets_root() {
|
||||
if (const char *e = std::getenv("M2_ASSETS")) {
|
||||
return e;
|
||||
}
|
||||
return "../../assets"; // ctest cwd = build/extension -> <repo>/assets
|
||||
}
|
||||
|
||||
static bool exists(const std::string &p) {
|
||||
FILE *f = std::fopen(p.c_str(), "rb");
|
||||
if (f) {
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// --- 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) {
|
||||
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.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;
|
||||
for (uint32_t i = 0; i < p.elements; ++i) {
|
||||
ItemRecord it = parse_item(p.record(i), p.stride);
|
||||
if (it.vnum && it.vnum <= prev) {
|
||||
mono = 0;
|
||||
}
|
||||
prev = it.vnum ? it.vnum : prev;
|
||||
if (!it.name.empty()) {
|
||||
++named;
|
||||
}
|
||||
if (it.specular > max_spec) {
|
||||
max_spec = it.specular;
|
||||
}
|
||||
}
|
||||
CHECK(mono, "item vnums non-decreasing");
|
||||
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).
|
||||
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);
|
||||
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(found_armor, "item_proto contains vnum 11209");
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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) {
|
||||
CHECK(p.fourcc == 0x54504D4Du, "mob fourcc MMPT");
|
||||
CHECK(p.stride == 335, "mob stride 335 (derived from realSize/elements)");
|
||||
CHECK(p.elements > 100, "mob elements > 100");
|
||||
|
||||
int named = 0, maxlvl = 0;
|
||||
for (uint32_t i = 0; i < p.elements; ++i) {
|
||||
MobRecord m = parse_mob(p.record(i), p.stride);
|
||||
if (!m.name.empty()) {
|
||||
++named;
|
||||
}
|
||||
if (m.level > maxlvl) {
|
||||
maxlvl = m.level;
|
||||
}
|
||||
}
|
||||
CHECK(named > p.elements * 0.8, "mob: >80% named");
|
||||
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);
|
||||
CHECK(first.vnum > 0 && first.vnum < 60000, "mob[0] vnum sane");
|
||||
CHECK(!first.name.empty(), "mob[0] has a name");
|
||||
}
|
||||
}
|
||||
|
||||
if (g_fail) {
|
||||
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
|
||||
return 1;
|
||||
}
|
||||
std::printf("all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user