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:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+140
View File
@@ -0,0 +1,140 @@
// MarkImageSet + the GC_MARK_* body parsers — synthetic packets in, guild-mark
// pixels out. No socket: this covers the parse/decompress/blit path that the
// MarkClient (mark_client.h) drives over the wire.
#include "../src/net/mark_image.h"
#include "../src/net/wire.h"
#include <lzo/lzo1x.h>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace mtnet;
static int g_fail = 0;
#define CHECK(c, msg) \
do { \
if (!(c)) { \
std::fprintf(stderr, "FAIL: %s\n", msg); \
++g_fail; \
} \
} while (0)
static void put_u16(std::vector<uint8_t> &b, uint16_t v) {
b.push_back((uint8_t)(v & 0xFF));
b.push_back((uint8_t)(v >> 8));
}
static void put_u32(std::vector<uint8_t> &b, uint32_t v) {
for (int i = 0; i < 4; ++i) {
b.push_back((uint8_t)((v >> (8 * i)) & 0xFF));
}
}
// LZO1X-compress one 64x48 RGBA block (MARK_BLOCK_PIXELS words).
static std::vector<uint8_t> compress_block(const uint32_t *px) {
lzo_init();
static std::vector<uint8_t> wrk(LZO1X_1_MEM_COMPRESS);
std::vector<uint8_t> out(MARK_BLOCK_PIXELS * 4 + MARK_BLOCK_PIXELS + 64);
lzo_uint out_len = out.size();
int r = lzo1x_1_compress((const uint8_t *)px, MARK_BLOCK_PIXELS * 4, out.data(), &out_len,
wrk.data());
if (r != LZO_E_OK) {
std::fprintf(stderr, "FAIL: lzo1x_1_compress -> %d\n", r);
++g_fail;
}
out.resize(out_len);
return out;
}
int main() {
// Guild 4242's mark lives at mark_id 1281 -> image 1, position 1 (row 0,
// col 1): pixel origin (16, 0). Guild 7 -> mark_id 3 -> image 0, pos 3.
const uint32_t kGuildA = 4242, kMarkA = MARK_PER_IMAGE + 1; // 1281
const uint32_t kGuildB = 7, kMarkB = 3;
// --- GC_MARK_IDXLIST body: count x {u16 guild_id, u16 mark_id} ---
MarkImageSet set;
{
std::vector<uint8_t> body;
put_u16(body, (uint16_t)kGuildA);
put_u16(body, (uint16_t)kMarkA);
put_u16(body, (uint16_t)kGuildB);
put_u16(body, (uint16_t)kMarkB);
size_t n = parse_mark_idxlist(body.data(), body.size(), 2, set);
CHECK(n == 2, "idxlist: 2 entries parsed");
}
CHECK(set.has_mark(kGuildA) && set.has_mark(kGuildB), "idxlist: both guilds registered");
CHECK(set.mark_id(kGuildA) == kMarkA, "idxlist: mark id stored");
CHECK(!set.has_mark(999), "idxlist: unknown guild absent");
// needed images = {0, 1} ascending
std::vector<int> need = set.needed_images();
CHECK(need.size() == 2 && need[0] == 0 && need[1] == 1, "needed_images = {0,1}");
// rect: guild A at image 1, (16, 0); guild B at image 0, (48, 0)
MarkRect ra = set.rect_of(kGuildA);
CHECK(ra.found && ra.img_idx == 1 && ra.x == 16 && ra.y == 0 && ra.w == 16 && ra.h == 12,
"rect_of(A) = img1 (16,0) 16x12");
MarkRect rb = set.rect_of(kGuildB);
CHECK(rb.found && rb.img_idx == 0 && rb.x == 48 && rb.y == 0, "rect_of(B) = img0 (48,0)");
// --- GC_MARK_BLOCK body for image 1, block 0 (covers marks at cols 0..3) ---
// Paint guild A's 16x12 cell (origin 16,0 within the block) a solid colour.
const uint32_t kColour = 0xFF3399CCu; // AABBGGRR
{
std::vector<uint32_t> block(MARK_BLOCK_PIXELS, 0);
for (int j = 0; j < GUILD_MARK_HEIGHT; ++j) {
for (int i = 0; i < GUILD_MARK_WIDTH; ++i) {
block[(size_t)j * MARK_BLOCK_WIDTH + (16 + i)] = kColour;
}
}
std::vector<uint8_t> comp = compress_block(block.data());
std::vector<uint8_t> body;
body.push_back(0); // block_pos 0
put_u32(body, (uint32_t)comp.size());
body.insert(body.end(), comp.begin(), comp.end());
size_t applied = parse_mark_block(body.data(), body.size(), /*img_idx=*/1, /*count=*/1, set);
CHECK(applied == 1, "block: 1 block applied");
}
// guild A's mark pixels should now be the solid colour; guild B (no block
// for image 0 yet) should come back empty.
std::vector<uint32_t> pa = set.mark_pixels(kGuildA);
CHECK(pa.size() == (size_t)(GUILD_MARK_WIDTH * GUILD_MARK_HEIGHT), "mark_pixels(A): 192 words");
bool all_colour = !pa.empty();
for (uint32_t p : pa) {
all_colour = all_colour && (p == kColour);
}
CHECK(all_colour, "mark_pixels(A): every pixel is the painted colour");
CHECK(set.mark_pixels(kGuildB).empty(), "mark_pixels(B): empty (image 0 not downloaded)");
// corrupt compressed data -> apply_block fails, image untouched
{
uint8_t junk[8] = {1, 2, 3, 4, 5, 6, 7, 8};
CHECK(!set.apply_block(1, 1, junk, sizeof(junk)), "apply_block: junk rejected");
}
// out-of-range indices
CHECK(!set.apply_block(-1, 0, (const uint8_t *)"x", 1), "apply_block: bad img idx");
CHECK(!set.apply_block(0, MARK_BLOCK_TOTAL_COUNT, (const uint8_t *)"x", 1),
"apply_block: bad block pos");
// truncated block body -> nothing applied, no crash
{
std::vector<uint8_t> body;
body.push_back(2);
put_u32(body, 9999); // claims 9999 bytes that aren't there
size_t applied = parse_mark_block(body.data(), body.size(), 0, 1, set);
CHECK(applied == 0, "block: truncated body -> 0 applied");
}
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
std::printf("PASS: net_mark_test\n");
return 0;
}