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
141 lines
5.0 KiB
C++
141 lines
5.0 KiB
C++
// 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;
|
|
}
|