Files
mtgodot-poc/extension/tests/pack_roundtrip_test.cpp
shenandClaude Sonnet 5 47baf6c0c6 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
2026-08-31 20:02:12 +09:00

144 lines
4.8 KiB
C++

// EterPack writer -> reader round trip (no external pack file needed).
#include "../src/pack/eterpack.h"
#include "../src/pack/pack_mount.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
using namespace mtpack;
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::vector<uint8_t> bytes(const std::string &s) {
return std::vector<uint8_t>(s.begin(), s.end());
}
static bool roundtrip(bool encrypt, const char *tag) {
std::vector<InputFile> in;
in.push_back({"d:/ymir work/pc/warrior/warrior.gr2", bytes(std::string(5000, 'A'))}); // compressible
{
std::vector<uint8_t> rnd(4096);
for (size_t i = 0; i < rnd.size(); ++i) {
rnd[i] = static_cast<uint8_t>((i * 2654435761u) >> 13);
}
in.push_back({"textureset/metin2_a1.txt", rnd});
}
in.push_back({"tiny.dat", bytes("x")});
in.push_back({"nested/deep/path/file.bin", bytes("hello \x00 world binary\xff\xfe")});
std::string path = std::string(std::getenv("TMPDIR") ? std::getenv("TMPDIR") : "/tmp") +
"/mtpack_test_" + tag + ".epk";
std::string err;
if (!write_pack(path, in, encrypt, &err)) {
std::fprintf(stderr, "write_pack(%s): %s\n", tag, err.c_str());
return false;
}
EterPack pk;
if (!pk.open(path, &err)) {
std::fprintf(stderr, "open(%s): %s\n", tag, err.c_str());
return false;
}
CHECK(pk.count() == in.size(), "entry count");
CHECK(pk.name_field() == PACK_NAME_FIELD_DEFAULT, "derived name field == default");
for (const auto &f : in) {
CHECK(pk.has(f.name), (std::string("has ") + f.name).c_str());
// case / slash insensitivity
std::string up = f.name;
for (auto &c : up) {
c = static_cast<char>(std::toupper((unsigned char)c));
}
std::replace(up.begin(), up.end(), '/', '\\');
CHECK(pk.has(up), (std::string("has (norm) ") + f.name).c_str());
std::vector<uint8_t> got;
if (!pk.read(f.name, got, &err)) {
std::fprintf(stderr, "read(%s): %s\n", f.name.c_str(), err.c_str());
++g_fail;
continue;
}
CHECK(got == f.data, (std::string("bytes match ") + f.name).c_str());
}
CHECK(!pk.read("does/not/exist", *(new std::vector<uint8_t>()), &err), "missing file -> false");
std::remove(path.c_str());
return true;
}
static std::string tmp(const char *n) {
return std::string(std::getenv("TMPDIR") ? std::getenv("TMPDIR") : "/tmp") + "/mtpack_" + n;
}
static bool mount_test() {
// pack A: base assets, "ymir work/" layout
std::vector<InputFile> a;
a.push_back({"ymir work/ui/pattern/board_base.tga", bytes("BASE-BOARD-v1")});
a.push_back({"ymir work/tree/b1_pagoda.spt", bytes(std::string(2000, 'S'))});
a.push_back({"locale/loading.png", bytes("PNGDATA")});
std::string pa = tmp("mount_a.epk");
std::string err;
if (!write_pack(pa, a, false, &err)) {
std::fprintf(stderr, "write A: %s\n", err.c_str());
return false;
}
// pack B: a patch that overrides board_base
std::vector<InputFile> b;
b.push_back({"ymir work/ui/pattern/board_base.tga", bytes("BASE-BOARD-v2-PATCHED")});
std::string pb = tmp("metin2_patch_x.epk");
if (!write_pack(pb, b, true, &err)) {
std::fprintf(stderr, "write B: %s\n", err.c_str());
return false;
}
mtpack::PackMount m;
CHECK(m.mount(pa, &err), ("mount A: " + err).c_str());
CHECK(m.mount(pb, &err), ("mount B: " + err).c_str());
CHECK(m.pack_count() == 2, "2 packs mounted");
std::vector<uint8_t> out;
// full path
CHECK(m.has("ymir work/tree/b1_pagoda.spt"), "has by full path");
CHECK(m.read("ymir work/tree/b1_pagoda.spt", out) && out.size() == 2000, "read by full path");
// virtual path with drive + backslashes + case
CHECK(m.has("D:\\YMIR WORK\\Tree\\B1_Pagoda.spt"), "has by d:\\ virtual path");
CHECK(m.read("d:/ymir work/tree/b1_pagoda.spt", out) && out.size() == 2000, "read by virtual");
// "ymir work/"-suffix keying: a vpath that omits the leading dirs
CHECK(m.read("somewhere/ymir work/ui/pattern/board_base.tga", out), "read via ymir-suffix");
// later pack (patch) wins
CHECK(std::string(out.begin(), out.end()) == "BASE-BOARD-v2-PATCHED", "patch pack overrides base");
// non-ymir path still works
CHECK(m.read("locale/loading.png", out) &&
std::string(out.begin(), out.end()) == "PNGDATA",
"non-ymir path");
CHECK(!m.has("does/not/exist"), "missing -> false");
std::remove(pa.c_str());
std::remove(pb.c_str());
return true;
}
int main() {
CHECK(roundtrip(false, "plain"), "plain roundtrip ran");
CHECK(roundtrip(true, "enc"), "encrypted roundtrip ran");
CHECK(mount_test(), "PackMount test ran");
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
std::printf("all checks passed\n");
return 0;
}