Files
mtgodot-poc/extension/tests/net_state_queue_test.cpp
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

373 lines
16 KiB
C++

// 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 <cmath>
#include <cstdio>
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) FUNC_SKILL: 40250 eFunc & FUNC_SKILL ----------------------------
// InstanceBase::StateProcess treats skill states like attack states: if the
// destination is far away, walk first and call NEW_UseSkill on arrival. A
// plain equality check loses the motion bits and turns the actor back to WAIT.
{
EntityStore es;
es.set_now(0);
spawn(es, 35, 0, 0);
const uint8_t skill_func = FUNC_SKILL | 7; // motion 7, same wire encoding
es.mut_move(35, 90.0f, skill_func, 220.0f, 0.0f, 0, /*arg*/ 3, 0);
CHECK(es.get(35)->moving && es.get(35)->func == FUNC_MOVE,
"FUNC_SKILL far: walks before casting");
CHECK(es.get(35)->mov_after_func == skill_func && es.get(35)->mov_after_arg == 3,
"FUNC_SKILL far: motion and skill arg latched for arrival");
es.set_now(100000);
es.tick();
CHECK(!es.get(35)->moving && es.get(35)->func == skill_func,
"FUNC_SKILL far: skill state survives arrival");
CHECK(es.get(35)->func_arg == 3,
"FUNC_SKILL far: OnUseSkill arg survives the walk-to-skill transition");
CHECK(es.get(35)->mov_after_func == FUNC_WAIT, "FUNC_SKILL latch cleared post-arrival");
// Near skills must also act immediately, not fall through to WAIT.
es.mut_move(35, 180.0f, FUNC_SKILL | 2, 230.0f, 0.0f, 0, 1, 0);
CHECK(!es.get(35)->moving && es.get(35)->func == (FUNC_SKILL | 2),
"FUNC_SKILL near: snaps and casts immediately");
CHECK(es.get(35)->func_arg == 1,
"FUNC_SKILL near: OnUseSkill arg is retained on immediate cast");
}
// --- 4b) FUNC_EMOTION: 100cm threshold + deferred action ---------------
// InstanceBase::StateProcess gives emotion its own threshold: a remote actor
// farther than 100cm walks first, then executes the emotion at the destination.
{
EntityStore es;
es.set_now(0);
spawn(es, 36, 0, 0);
es.mut_move(36, 90.0f, FUNC_EMOTION, 150.0f, 0.0f, 0, /*arg*/ 42, 0);
CHECK(es.get(36)->moving && es.get(36)->func == FUNC_MOVE,
"FUNC_EMOTION far: walks before emotion");
CHECK(es.get(36)->mov_after_func == FUNC_EMOTION && es.get(36)->mov_after_arg == 42,
"FUNC_EMOTION far: emotion is latched for arrival");
CHECK(!es.get(36)->skip_collision,
"FUNC_EMOTION far: remote emotion walk keeps actor collision enabled");
es.set_now(100000);
es.tick();
CHECK(!es.get(36)->moving && es.get(36)->func == FUNC_EMOTION &&
es.get(36)->func_arg == 42,
"FUNC_EMOTION far: emotion survives arrival");
es.mut_move(36, 180.0f, FUNC_EMOTION, 180.0f, 0.0f, 0, /*arg*/ 7, 0);
CHECK(!es.get(36)->moving && es.get(36)->func == FUNC_EMOTION &&
es.get(36)->func_arg == 7,
"FUNC_EMOTION near: snaps and acts immediately");
}
// --- 5) __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");
}
// --- 5b) skill/emotion TCP gates -----------------------------------------
// 40250 checks both __CanProcessNetworkStatePacket (non-cancellable skill)
// and __IsEnableTCPProcess (active emotion / disabled TCP state). The gate
// is presentation-owned and must release the already queued command when
// the one-shot motion reaches its tail.
{
EntityStore es;
es.set_now(0);
spawn(es, 43, 0, 0);
es.mut_network_state_gate(43, true, false, false, true);
es.mut_move(43, 0.0f, FUNC_MOVE, 300.0f, 0.0f, 0, 0, 0);
CHECK(es.get(43)->state_queue.size() == 1 && !es.get(43)->moving,
"non-cancellable skill: state cmd held");
es.mut_network_state_gate(43, false, true, false, true);
es.process_states();
CHECK(es.get(43)->state_queue.empty() && es.get(43)->moving,
"skill motion tail: queued state releases");
spawn(es, 44, 0, 0);
es.mut_network_state_gate(44, false, true, true, false);
es.mut_move(44, 0.0f, FUNC_MOVE, 300.0f, 0.0f, 0, 0, 0);
CHECK(es.get(44)->state_queue.size() == 1 && !es.get(44)->moving,
"active emotion: later TCP state held");
es.mut_network_state_gate(44, false, true, false, true);
es.process_states();
CHECK(es.get(44)->state_queue.empty() && es.get(44)->moving,
"emotion motion tail: queued state releases");
spawn(es, 45, 0, 0);
es.mut_network_state_gate(45, false, true, false, false);
es.mut_move(45, 0.0f, FUNC_MOVE, 300.0f, 0.0f, 0, 0, 0);
CHECK(es.get(45)->state_queue.size() == 1 && !es.get(45)->moving,
"TCP disabled: non-emotion state held");
spawn(es, 46, 0, 0);
es.mut_network_state_gate(46, false, true, false, false);
es.mut_move(46, 0.0f, FUNC_EMOTION, 0.5f, 0.0f, 0, 9, 0);
CHECK(es.get(46)->state_queue.empty() && es.get(46)->func == FUNC_EMOTION,
"TCP disabled: FUNC_EMOTION remains allowed by eCurFunc exception");
}
// --- 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");
}
// --- 8b) remote NPC/MOB FUNC_MOVE keeps advancing past Dst ---------------
// 40250 MovementProcess has one non-main branch for PCs, NPCs and monsters.
// When m_kMovAfterFunc is FUNC_MOVE, the arrival switch deliberately does
// nothing: the actor remains in motion and the next network command (or the
// >100cm overshoot guard) owns the eventual stop. A ch_type-based early stop
// is therefore not equivalent and makes remote mobs stutter at every packet.
{
EntityStore es;
es.set_now(0);
es.mut_spawn(81, 101, 2, "Wolf", 0, 0, 0, 0, 100, 0);
es.set_motion_speed(81, 0.0f, 1000.0f);
es.tick();
es.mut_move(81, 0.0f, FUNC_MOVE, 100.0f, 0.0f, 0, 0, 0);
es.set_now(100);
es.tick();
CHECK(es.get(81)->moving && es.get(81)->x >= 100.0f &&
es.get(81)->mov_after_func == FUNC_MOVE && es.get(81)->skip_collision,
"remote NPC/MOB FUNC_MOVE: arrival does not stop network walk");
}
// --- 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");
}
// --- 10) POINT_EXP: preserve the authoritative packet delta --------------
// 40250 shows the point effect from TPacketGCPointChange.amount. The
// absolute value can decrease at level-up, so comparing only old/new EXP is
// not equivalent.
{
EntityStore es;
es.mut_spawn_main(1, 1, "Hero", 0, 0, 0);
es.mut_set_point(POINT_EXP, 5, 1, 125);
es.mut_set_point(POINT_EXP, 20, 1, 15);
CHECK(es.take_points_dirty(), "POINT_EXP changes dirty player points");
CHECK(es.take_exp_gain() == 140, "POINT_EXP amount accumulates across one pump");
CHECK(es.take_exp_gain() == 0, "POINT_EXP gain is consumed exactly once");
}
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;
}