// CLIENT-GAP §3.2 — EntityStore TCP state queue: server-frame release gate, // __CanProcessNetworkStatePacket gate, and the FUNC_* switch with its 1.0 / 50.0 // hard thresholds (no lerp, no jitter buffer). Drives the protocol-neutral // mutation API directly (same path the classic backend uses). #include "../src/net/entity_store.h" #include #include 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 spawn(EntityStore &es, uint32_t vid, float x, float y) { es.mut_spawn(vid, 101, 0, "Mob", x, y, 0.0f, 0.0f, 200 /*moving_speed*/, 0); } int main() { // --- 1) server-frame release gate --------------------------------------- { EntityStore es; es.set_now(0); spawn(es, 10, 0, 0); es.set_server_frame_ms(1000); // command timestamped in the future: stays queued, entity does not move. es.mut_move(10, 0.0f, FUNC_MOVE, 500.0f, 0.0f, 0, 0, /*chk_time*/ 2000); CHECK(es.get(10)->state_queue.size() == 1, "future cmd stays queued"); CHECK(!es.get(10)->moving, "future cmd: not moving yet"); es.process_states(); CHECK(es.get(10)->state_queue.size() == 1, "still queued below chk_time"); es.set_server_frame_ms(2000); // clock reaches the command time es.process_states(); CHECK(es.get(10)->state_queue.empty(), "cmd released once frame clock catches up"); CHECK(es.get(10)->moving && es.get(10)->func == FUNC_MOVE, "released FUNC_MOVE walks"); // frame clock 0 == "not aligned yet" -> release immediately. EntityStore es0; es0.set_now(0); spawn(es0, 11, 0, 0); es0.mut_move(11, 0.0f, FUNC_MOVE, 500.0f, 0.0f, 0, 0, /*chk_time*/ 9999); CHECK(es0.get(11)->state_queue.empty() && es0.get(11)->moving, "frame clock 0 releases regardless of chk_time"); } // --- 2) FUNC_WAIT: 1.0 threshold (snap-align vs. walk) ------------------ { EntityStore es; es.set_now(0); spawn(es, 20, 0, 0); // within 1.0 -> snap align, no walk. es.mut_move(20, 90.0f, FUNC_WAIT, 0.5f, 0.0f, 0, 0, 0); CHECK(!es.get(20)->moving, "FUNC_WAIT dir_len<=1.0: no walk"); CHECK(std::abs(es.get(20)->x - 0.5f) < 1e-3f, "FUNC_WAIT dir_len<=1.0: snapped to Dst"); CHECK(es.get(20)->func == FUNC_WAIT && std::abs(es.get(20)->angle - 90.0f) < 1e-3f, "FUNC_WAIT snap: func + heading applied"); // beyond 1.0 -> walk there, m_kMovAfterFunc == WAIT. es.mut_move(20, 0.0f, FUNC_WAIT, 100.0f, 0.0f, 0, 0, 0); CHECK(es.get(20)->moving && es.get(20)->func == FUNC_MOVE, "FUNC_WAIT dir_len>1.0: walks"); CHECK(es.get(20)->mov_after_func == FUNC_WAIT, "FUNC_WAIT walk: after-func WAIT"); CHECK(es.get(20)->skip_collision, "network-driven walk enables skip-collision"); es.set_now(100000); es.tick(); CHECK(!es.get(20)->moving && es.get(20)->func == FUNC_WAIT, "FUNC_WAIT walk: settles to WAIT"); CHECK(!es.get(20)->skip_collision, "skip-collision cleared on arrival"); } // --- 3) FUNC_COMBO: 50.0 threshold + after-arrival action -------------- { EntityStore es; es.set_now(0); spawn(es, 30, 0, 0); // near (dir_len 20 < 50) -> snap + act now, arg is on the row's func. es.mut_move(30, 45.0f, FUNC_COMBO, 20.0f, 0.0f, 0, /*arg*/ 7, 0); CHECK(!es.get(30)->moving && es.get(30)->func == FUNC_COMBO, "FUNC_COMBO dir_len<50: snap + act"); CHECK(std::abs(es.get(30)->x - 20.0f) < 1e-3f, "FUNC_COMBO near: snapped to Dst"); // far (dir_len 200 >= 50) -> walk first, combat + arg deferred to arrival. es.mut_move(30, 0.0f, FUNC_COMBO, 220.0f, 0.0f, 0, /*arg*/ 9, 0); CHECK(es.get(30)->moving && es.get(30)->func == FUNC_MOVE, "FUNC_COMBO dir_len>=50: walks"); CHECK(es.get(30)->mov_after_func == FUNC_COMBO && es.get(30)->mov_after_arg == 9, "FUNC_COMBO far: combo + motion index latched for arrival"); es.set_now(100000); es.tick(); CHECK(!es.get(30)->moving && es.get(30)->func == FUNC_COMBO, "FUNC_COMBO far: combo runs on arrival"); CHECK(es.get(30)->mov_after_func == FUNC_WAIT, "after-func latch cleared post-arrival"); } // --- 4) __CanProcessNetworkStatePacket gate (dead / knocked-down) ------ { EntityStore es; es.set_now(0); spawn(es, 40, 0, 0); es.mut_knockdown(40, true); es.mut_move(40, 0.0f, FUNC_MOVE, 300.0f, 0.0f, 0, 0, 0); CHECK(es.get(40)->state_queue.size() == 1 && !es.get(40)->moving, "knocked-down: state cmd held, not applied"); es.mut_knockdown(40, false); es.process_states(); CHECK(es.get(40)->state_queue.empty() && es.get(40)->moving, "recovered: held state cmd releases"); es.mut_dead(41); // unknown vid: no-op, must not crash spawn(es, 42, 0, 0); es.mut_dead(42); es.mut_move(42, 0.0f, FUNC_MOVE, 300.0f, 0.0f, 0, 0, 0); CHECK(!es.get(42)->moving && es.get(42)->state_queue.size() == 1, "dead: state cmd held"); } // --- 5) queue preserves order and drains multiple due commands -------- { EntityStore es; es.set_now(0); es.set_server_frame_ms(0); spawn(es, 50, 0, 0); es.mut_move(50, 0.0f, FUNC_WAIT, 0.2f, 0.0f, 0, 0, 0); // snap (near) es.mut_move(50, 0.0f, FUNC_ATTACK, 10.0f, 0.0f, 0, 3, 0); // near attack -> snap + act CHECK(es.get(50)->state_queue.empty(), "both due cmds drained in one pass"); CHECK(es.get(50)->func == FUNC_ATTACK && std::abs(es.get(50)->x - 10.0f) < 1e-3f, "last cmd wins: ATTACK at (10,0)"); } // --- 6) staleness valve: chk_time far ahead of the frame clock ----------- // Reference StateProcess has no cap; ours releases a command whose chk_time // is > STATE_QUEUE_MAX_WAIT_MS ahead of the (unvalidated) server-frame clock // so a wrong-timebase dwTime can never freeze the actor forever. { EntityStore es; es.set_now(0); es.set_server_frame_ms(1000); // exactly at the cap -> still gated. spawn(es, 60, 0, 0); es.mut_move(60, 0.0f, FUNC_MOVE, 500.0f, 0.0f, 0, 0, /*chk_time*/ 1000 + STATE_QUEUE_MAX_WAIT_MS); CHECK(es.get(60)->state_queue.size() == 1 && !es.get(60)->moving, "chk_time == frame+cap: still gated"); // one ms past the cap -> treated as out-of-band, released now. spawn(es, 61, 0, 0); es.mut_move(61, 0.0f, FUNC_MOVE, 500.0f, 0.0f, 0, 0, /*chk_time*/ 1000 + STATE_QUEUE_MAX_WAIT_MS + 1); CHECK(es.get(61)->state_queue.empty() && es.get(61)->moving && es.get(61)->func == FUNC_MOVE, "chk_time past cap: released immediately (staleness valve)"); } // --- 7) motion-driven remote walk (reference MovementProcess) -------------- // Speed comes from the WALK/RUN root motion * movSpd/100, not dwDuration; // heading follows Src->Dst while walking and takes bRot on arrival. { EntityStore es; es.set_now(0); es.mut_spawn(70, 101, 2, "Wolf", 0, 0, 0, 0, 100 /*moving_speed*/, 0); es.set_motion_speed(70, 87.5f, 425.0f); es.tick(); // server says 100 ms (it prices NPC walks at RUN speed); ignored here. es.mut_move(70, 45.0f, FUNC_WAIT, 1000.0f, 0.0f, 100, 0, 0); CHECK(es.get(70)->moving && std::abs(es.get(70)->angle - 90.0f) < 1e-3f, "motion walk: faces Src->Dst, not bRot"); uint32_t now = 0; for (int i = 0; i < 10; ++i) { now += 100; es.set_now(now); es.tick(); } CHECK(es.get(70)->moving && std::abs(es.get(70)->x - 425.0f) < 1.0f, "motion walk: run mode advances at RUN motion speed, ignores dwDuration"); es.mut_walk_mode(70, WALKMODE_WALK); for (int i = 0; i < 10; ++i) { now += 100; es.set_now(now); es.tick(); } CHECK(std::abs(es.get(70)->x - 512.5f) < 1.0f, "motion walk: walk mode uses WALK motion speed"); es.mut_change_speed(70, 200); // movSpd 200 doubles the step now += 1000; es.set_now(now); es.tick(); // capped at MOVE_MAX_TICK_MS CHECK(std::abs(es.get(70)->x - (512.5f + 175.0f * 0.25f)) < 1.0f, "motion walk: movSpd scales speed; tick step capped"); for (int i = 0; i < 100 && es.get(70)->moving; ++i) { now += 100; es.set_now(now); es.tick(); } CHECK(!es.get(70)->moving && es.get(70)->func == FUNC_WAIT, "motion walk: stops on arrival"); CHECK(es.get(70)->x == 1000.0f && es.get(70)->y == 0.0f, "motion walk: snaps to Dst"); CHECK(std::abs(es.get(70)->angle - 45.0f) < 1e-3f, "motion walk: bRot applied on arrival"); } // --- 8) FUNC_MOVE walks past Dst, overshoot guard turns back and stops ------ { EntityStore es; es.set_now(0); es.mut_spawn(80, 0, 0, "Pc", 0, 0, 0, 0, 100, 0); es.set_motion_speed(80, 0.0f, 1000.0f); es.tick(); es.mut_move(80, 90.0f, FUNC_MOVE, 100.0f, 0.0f, 100, 0, 0); uint32_t now = 0; for (int i = 0; i < 3; ++i) { now += 50; es.set_now(now); es.tick(); } CHECK(es.get(80)->moving && es.get(80)->x > 100.0f, "FUNC_MOVE: keeps walking past Dst"); bool turned = false; for (int i = 0; i < 40 && es.get(80)->moving; ++i) { now += 50; es.set_now(now); es.tick(); turned = turned || std::abs(es.get(80)->angle - 270.0f) < 1e-3f; } CHECK(turned, "FUNC_MOVE overshoot: turns back toward Dst"); CHECK(!es.get(80)->moving && es.get(80)->x == 100.0f && es.get(80)->func == FUNC_WAIT, "FUNC_MOVE overshoot: returns to Dst and stops"); } // --- 9) unknown motion speed keeps the dwDuration lerp, heading Src->Dst ----- { EntityStore es; es.set_now(0); spawn(es, 90, 0, 0); es.mut_move(90, 0.0f, FUNC_WAIT, 0.0f, -500.0f, 1000, 0, 0); CHECK(std::abs(es.get(90)->angle - 180.0f) < 1e-3f, "duration walk: faces Src->Dst"); es.set_now(500); es.tick(); CHECK(std::abs(es.get(90)->y + 250.0f) < 1e-3f, "duration walk: lerps over dwDuration"); } if (g_fail == 0) { std::printf("PASS: net_state_queue_test (§3.2 TCP state queue / thresholds / gates)\n"); } return g_fail == 0 ? 0 : 1; }