完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+44
View File
@@ -0,0 +1,44 @@
// Public M2Client argument bounds shared by the Classic and m2dev backends.
#include "../src/net/net_bounds.h"
#include <cstdio>
static int g_fail = 0;
#define CHECK(c, msg) \
do { \
if (!(c)) { \
std::fprintf(stderr, "FAIL: %s\n", msg); \
++g_fail; \
} \
} while (0)
int main() {
using namespace mtnet::bounds;
CHECK(u8(0) && u8(255), "u8 accepts wire range");
CHECK(!u8(-1) && !u8(256), "u8 rejects outside range");
CHECK(u16(65535) && !u16(-1) && !u16(65536), "u16 range");
CHECK(u32(0) && u32(0xffffffffLL) && !u32(-1), "u32 range");
CHECK(i32(-2147483648LL) && i32(2147483647LL), "i32 accepts signed range");
CHECK(!i32(-2147483649LL) && !i32(2147483648LL), "i32 rejects outside range");
CHECK(quick_slot(0) && quick_slot(35), "quick slot endpoints");
CHECK(!quick_slot(-1) && !quick_slot(36), "quick slot outside range");
CHECK(private_shop_slot(0) && private_shop_slot(39), "private shop endpoints");
CHECK(!private_shop_slot(40), "private shop outside range");
CHECK(exchange_slot(0) && exchange_slot(11) && !exchange_slot(12), "exchange range");
CHECK(cube_slot(0) && cube_slot(23) && !cube_slot(24), "cube range");
// §3.8 mod 3: use_skill skill index. 1..254 accepted, 0 / >=255 / negative rejected.
CHECK(SKILL_MAX_NUM == 255, "SKILL_MAX_NUM matches Packet.h");
CHECK(skill_index(1) && skill_index(254), "skill index endpoints");
CHECK(!skill_index(0) && !skill_index(255) && !skill_index(-1), "skill index outside range");
CHECK(!skill_index(0x7fffffffLL + 1), "skill index rejects large value");
if (g_fail) {
std::fprintf(stderr, "%d checks FAILED\n", g_fail);
return 1;
}
std::puts("net_bounds_test: OK");
return 0;
}
+7 -1
View File
@@ -871,9 +871,15 @@ int main() {
ex.arg1 = 27001;
ex.arg2 = {0, 4};
ex.arg3 = 7;
ex.sockets[1] = 9002;
ex.attrs[2] = {17, -33};
feed(s, raw(ex));
CHECK(s.world().exchange().self_items[4].vnum == 27001 &&
s.world().exchange().self_items[4].count == 7, "exchange item add");
s.world().exchange().self_items[4].count == 7 &&
s.world().exchange().self_items[4].sockets[1] == 9002 &&
s.world().exchange().self_items[4].attrs[2].type == 17 &&
s.world().exchange().self_items[4].attrs[2].value == -33,
"exchange item add keeps sockets/attrs");
ex.subheader = EXCHANGE_GC_GOLD_ADD;
ex.arg1 = 123456;
@@ -78,6 +78,30 @@ int main() {
CHECK(out[sizeof(Handshake)] == SEQUENCE_TABLE[0], "time-sync uses next sequence byte");
}
// -------------------------------------------- server frame clock (§3.2)
{
ClassicStream s;
CHECK(s.server_frame_ms() == 0, "server_frame_ms is 0 before any handshake");
CHECK(!s.has_server_clock(), "has_server_clock false before handshake");
Handshake hs{};
hs.header = HDR_HANDSHAKE;
hs.handshake = 0x1234;
hs.time = 500000;
hs.delta = 20;
auto b = raw(hs);
s.feed(b.data(), b.size());
(void)drain(s);
CHECK(s.has_server_clock(), "has_server_clock true after handshake");
uint32_t base = 500000u + 20u; // hs.time + lDelta
uint32_t f1 = s.server_frame_ms();
CHECK(f1 >= base && f1 < base + 5000u,
"server_frame_ms ~ hs.time + lDelta right after handshake");
uint32_t f2 = s.server_frame_ms();
CHECK(f2 >= f1, "server frame clock is monotonic non-decreasing");
}
// ---------------------------------------------------------- GC_PHASE
{
ClassicStream s;
+34 -5
View File
@@ -165,20 +165,41 @@ int main() {
CHECK(!es.get(2)->moving, "stopped at t=1");
CHECK(es.get(2)->func == FUNC_WAIT, "func back to WAIT");
// --- a non-move func snaps ---
// --- §3.2 state queue: a near attack func (dir_len < 50) snaps and acts ---
// entity 2 is at (1500,500); target (1530,500) -> dir_len 30 < 50.
GCMove atk{};
atk.header = GC_MOVE;
atk.length = sizeof(atk);
atk.vid = 2;
atk.func = FUNC_ATTACK;
atk.x = 1600;
atk.y = 600;
atk.x = 1530;
atk.y = 500;
atk.duration = 0;
b = pkt(atk);
es.apply(GC_MOVE, b.data(), (uint16_t)b.size());
CHECK(!es.get(2)->moving && es.get(2)->x == 1600, "attack func snaps position");
CHECK(!es.get(2)->moving && es.get(2)->x == 1530, "near attack func snaps position");
CHECK(es.get(2)->func == FUNC_ATTACK, "func = ATTACK");
// --- §3.2 state queue: a far attack func (dir_len >= 50) walks, then attacks
// on arrival (reference m_kMovAfterFunc). ---
GCMove fatk{};
fatk.header = GC_MOVE;
fatk.length = sizeof(fatk);
fatk.vid = 2;
fatk.func = FUNC_ATTACK;
fatk.x = 1700; // from (1530,500): dir_len 170 >= 50
fatk.y = 500;
fatk.duration = 0;
b = pkt(fatk);
es.set_now(2000);
es.apply(GC_MOVE, b.data(), (uint16_t)b.size());
CHECK(es.get(2)->moving && es.get(2)->func == FUNC_MOVE, "far attack func walks first");
es.set_now(20000); // well past arrival
es.tick();
CHECK(!es.get(2)->moving && std::abs(es.get(2)->x - 1700.0f) < 0.01f,
"far attack func: arrived at Dst");
CHECK(es.get(2)->func == FUNC_ATTACK, "far attack func: attacks on arrival");
// --- movement metadata + batched correction packets ---
{
GCChangeSpeed speed{GC_CHANGE_SPEED, sizeof(GCChangeSpeed), 2, 175};
@@ -964,9 +985,17 @@ int main() {
xi.arg1 = 19; // vnum
xi.arg2.cell = 2; // exchange slot
xi.arg3 = 1; // count
xi.sockets[0] = 7001;
xi.sockets[2] = -1;
xi.attrs[0] = {5, 42};
b = pkt(xi);
es.apply(GC_EXCHANGE, b.data(), (uint16_t)b.size());
CHECK(es.exchange().self_items[2].vnum == 19, "exchange: self item add");
CHECK(es.exchange().self_items[2].vnum == 19 &&
es.exchange().self_items[2].sockets[0] == 7001 &&
es.exchange().self_items[2].sockets[2] == -1 &&
es.exchange().self_items[2].attrs[0].type == 5 &&
es.exchange().self_items[2].attrs[0].value == 42,
"exchange: self item add keeps sockets/attrs");
GCExchange xe{};
xe.header = GC_EXCHANGE;
+159
View File
@@ -0,0 +1,159 @@
// 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) __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)");
}
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;
}
@@ -0,0 +1,79 @@
// Synthetic item_proto record test. It locks the offsets used by the
// ClientVS22 TItemTable layout without requiring the 2.2G asset tree.
#include "../src/proto/proto.h"
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
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_u32(std::array<uint8_t, 236> &record, size_t offset, uint32_t value) {
record[offset + 0] = static_cast<uint8_t>(value);
record[offset + 1] = static_cast<uint8_t>(value >> 8);
record[offset + 2] = static_cast<uint8_t>(value >> 16);
record[offset + 3] = static_cast<uint8_t>(value >> 24);
}
int main() {
std::array<uint8_t, 236> record{};
put_u32(record, 0, 12345);
put_u32(record, 4, 7);
std::memcpy(record.data() + 8, "TestItem", 9);
std::memcpy(record.data() + 73, "LocaleItem", 11);
record[138] = 3;
record[139] = 4;
record[140] = 2;
record[141] = 1;
put_u32(record, 142, 1u << 8);
put_u32(record, 146, 1u << 3);
put_u32(record, 150, 0x12345678);
put_u32(record, 158, 9000);
put_u32(record, 162, 6000);
record[166] = 1;
put_u32(record, 167, 42);
record[176] = 2;
put_u32(record, 177, -7);
put_u32(record, 191 + 3 * 4, 3);
put_u32(record, 215, 1001);
put_u32(record, 219, -1);
put_u32(record, 227, 12346);
record[231] = 0x34;
record[232] = 0x12;
record[233] = 11;
record[234] = 75;
record[235] = 88;
const mtproto::ItemRecord item = mtproto::parse_item(record.data(), record.size());
CHECK(item.vnum == 12345, "vnum offset");
CHECK(item.vnum_range == 7, "vnum range offset");
CHECK(item.name == "TestItem", "name offset");
CHECK(item.locale_name == "LocaleItem", "locale name offset");
CHECK(item.type == 3 && item.sub_type == 4, "type offsets");
CHECK(item.anti_flags == (1u << 8), "anti_flags offset");
CHECK(item.flags == (1u << 3), "flags offset");
CHECK(item.wear_flags == 0x12345678, "wear_flags offset");
CHECK(item.buy_price == 9000 && item.sell_price == 6000, "price offsets");
CHECK(item.limits[0].type == 1 && item.limits[0].value == 42, "limit offsets");
CHECK(item.applies[0].type == 2 && item.applies[0].value == -7, "apply offsets");
CHECK(item.values[3] == 3, "values offset");
CHECK(item.sockets[0] == 1001 && item.sockets[1] == -1, "socket offsets");
CHECK(item.refined_vnum == 12346 && item.refine_set == 0x1234, "refine offsets");
CHECK(item.alter_to_magic_pct == 11 && item.specular == 75 && item.gain_socket_pct == 88,
"tail offsets");
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
std::printf("all item layout checks passed\n");
return 0;
}