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
144 lines
4.8 KiB
C++
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;
|
|
}
|