完善客户端功能并同步差距文档
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user