Files
mtgodot-poc/extension/tests/net_classic_stream_test.cpp
T

284 lines
9.0 KiB
C++

// net_classic_stream_test — 40250 "classic" framing layer, socket-free.
// Drives ClassicStream via feed()/take_outgoing() with synthetic packets.
#include "../src/net/classic/classic_stream.h"
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
using namespace mtnet::classic;
static int g_fail = 0;
#define CHECK(c, msg) \
do { \
if (!(c)) { \
std::fprintf(stderr, "FAIL: %s\n", msg); \
++g_fail; \
} \
} while (0)
template <class T>
static std::vector<uint8_t> raw(const T &s) {
std::vector<uint8_t> b(sizeof(T));
std::memcpy(b.data(), &s, sizeof(T));
return b;
}
static std::vector<uint8_t> drain(ClassicStream &s) {
std::vector<uint8_t> out;
uint8_t buf[4096];
for (;;) {
size_t n = s.take_outgoing(buf, sizeof(buf));
if (!n) {
break;
}
out.insert(out.end(), buf, buf + n);
}
return out;
}
int main() {
// ---------------------------------------------------------- handshake echo
{
ClassicStream s;
Handshake hs{};
hs.header = HDR_HANDSHAKE;
hs.handshake = 0xDEADBEEF;
hs.time = 1000;
hs.delta = 50;
auto b = raw(hs);
s.feed(b.data(), b.size());
auto out = drain(s);
CHECK(out.size() == sizeof(Handshake), "handshake echo is 13 bytes (no seq)");
Handshake echo{};
std::memcpy(&echo, out.data(), sizeof(echo));
CHECK(echo.header == HDR_HANDSHAKE, "echo keeps header 0xFF");
CHECK(echo.handshake == 0xDEADBEEF, "echo keeps handshake id");
CHECK(echo.time == 1000u + 2u * 50u, "echo time = time + 2*delta");
CHECK(echo.delta == 0, "echo delta = 0");
Handshake resync{};
resync.header = HDR_HANDSHAKE;
resync.handshake = 0xDEADBEEF;
resync.time = 2000;
resync.delta = 10;
s.set_sequence_mode(true);
s.set_time_sync_mode(true);
b = raw(resync);
s.feed(b.data(), b.size());
out = drain(s);
CHECK(out.size() == sizeof(Handshake) + 1, "later handshake uses time-sync + seq");
Handshake sync_echo{};
std::memcpy(&sync_echo, out.data(), sizeof(sync_echo));
CHECK(sync_echo.header == HDR_CG_TIME_SYNC, "later handshake header is CG_TIME_SYNC");
CHECK(sync_echo.time == 2000u + 2u * 10u && sync_echo.delta == 0,
"later handshake time fields are converged");
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;
int seen = -1;
s.on_phase = [&](uint8_t p) { seen = p; };
Phase_ p{HDR_GC_PHASE, PHASE_LOGIN};
auto b = raw(p);
s.feed(b.data(), b.size());
CHECK(seen == PHASE_LOGIN, "GC_PHASE delivers phase 2");
}
// ---------------------------------------------------------- GC_PING -> CG_PONG
{
ClassicStream s;
uint8_t ping = HDR_GC_PING; // 44
s.feed(&ping, 1);
auto out = drain(s);
CHECK(out.size() == 1 && out[0] == HDR_CG_PONG, "PING -> bare CG_PONG (seq off)");
s.set_sequence_mode(true);
s.feed(&ping, 1);
out = drain(s);
CHECK(out.size() == 2 && out[0] == HDR_CG_PONG && out[1] == SEQUENCE_TABLE[0],
"PING -> CG_PONG + seq[0] (seq on)");
}
// ---------------------------------------------------------- static GC packet
{
ClassicStream s;
uint8_t got_hdr = 0;
uint32_t got_len = 0;
std::vector<uint8_t> got_body;
s.on_packet = [&](uint8_t h, const uint8_t *body, uint32_t len) {
got_hdr = h;
got_len = len;
got_body.assign(body, body + len);
return true;
};
GCMainCharacter mc{};
mc.header = HDR_GC_MAIN_CHARACTER;
mc.vid = 0x0A0B0C0D;
mc.race = 4;
std::strcpy(mc.name, "Hero");
mc.x = 123456;
mc.y = 654321;
mc.z = 7;
mc.empire = 2;
mc.skill_group = 1;
auto b = raw(mc);
s.feed(b.data(), b.size());
CHECK(got_hdr == HDR_GC_MAIN_CHARACTER, "static: header 113 delivered");
CHECK(got_len == sizeof(GCMainCharacter) - 1, "static: body len = sizeof-1");
// body starts right after the header byte -> first 4 bytes are the vid
uint32_t vid;
std::memcpy(&vid, got_body.data(), 4);
CHECK(vid == 0x0A0B0C0D, "static: body begins at vid");
}
// ---------------------------------------------------------- dynamic GC packet
{
ClassicStream s;
uint8_t got_hdr = 0;
uint32_t got_len = 0;
std::string got_tail;
s.on_packet = [&](uint8_t h, const uint8_t *body, uint32_t len) {
got_hdr = h;
got_len = len;
got_tail.assign(reinterpret_cast<const char *>(body), len);
return true;
};
// GC_CHAT: [header][u16 size][type][u32 vid][empire] + "hi"
const char *msg = "hi";
GCChatHead ch{};
ch.header = HDR_GC_CHAT;
ch.type = 1;
ch.vid = 42;
ch.empire = 3;
ch.size = static_cast<uint16_t>(sizeof(GCChatHead) + std::strlen(msg));
std::vector<uint8_t> pkt = raw(ch);
pkt.insert(pkt.end(), msg, msg + std::strlen(msg));
s.feed(pkt.data(), pkt.size());
CHECK(got_hdr == HDR_GC_CHAT, "dynamic: GC_CHAT delivered");
// body = everything after [header][u16 size] => (type,vid,empire,"hi")
CHECK(got_len == ch.size - sizeof(DynHead), "dynamic: body len = size - 3");
CHECK(got_tail.size() >= 2 && got_tail.substr(got_tail.size() - 2) == "hi",
"dynamic: text tail present");
}
// ---------------------------------------------------------- send seq sequencing
{
ClassicStream s;
s.set_sequence_mode(true);
CGMove mv{};
mv.header = HDR_CG_MOVE;
mv.func = 1;
mv.x = 10;
mv.y = 20;
s.send_fixed(raw(mv).data(), sizeof(mv));
s.send_fixed(raw(mv).data(), sizeof(mv));
auto out = drain(s);
CHECK(out.size() == 2 * (sizeof(CGMove) + 1), "two CG_MOVE + 2 seq bytes");
CHECK(out[sizeof(CGMove)] == SEQUENCE_TABLE[0], "first seq = table[0]");
CHECK(out[2 * sizeof(CGMove) + 1] == SEQUENCE_TABLE[1], "second seq = table[1]");
CHECK(s.sequence_index() == 2, "seq index advanced to 2");
}
// seq off => no trailing byte
{
ClassicStream s;
CGMove mv{};
mv.header = HDR_CG_MOVE;
s.send_fixed(raw(mv).data(), sizeof(mv));
CHECK(drain(s).size() == sizeof(CGMove), "seq off: no trailing byte");
}
// ---------------------------------------------------------- partial feed
{
ClassicStream s;
int hits = 0;
s.on_packet = [&](uint8_t, const uint8_t *, uint32_t) {
++hits;
return true;
};
GCCharacterDel del{HDR_GC_CHARACTER_DEL, 99};
auto b = raw(del);
s.feed(b.data(), 2); // header + 1 byte of vid
CHECK(hits == 0, "partial: nothing delivered yet");
s.feed(b.data() + 2, b.size() - 2);
CHECK(hits == 1, "partial: delivered once the rest arrives");
}
// ---------------------------------------------------------- unknown header
{
ClassicStream s;
std::string err;
s.on_error = [&](const std::string &e) { err = e; };
uint8_t bad = 200; // ROULETTE — not in the size table yet
s.feed(&bad, 1);
CHECK(!err.empty(), "unknown header -> on_error");
CHECK(s.state() == ClassicStream::State::Offline, "unknown header -> disconnect");
GCCharacterDel del{HDR_GC_CHARACTER_DEL, 99};
auto trailing = raw(del);
s.feed(trailing.data(), trailing.size());
CHECK(s.outgoing_pending() == 0, "unknown header does not resync into trailing bytes");
}
// ---------------------------------------------------------- rejected handler
{
ClassicStream s;
std::string err;
s.on_error = [&](const std::string &e) { err = e; };
s.on_packet = [&](uint8_t, const uint8_t *, uint32_t) { return false; };
GCCharacterDel del{HDR_GC_CHARACTER_DEL, 99};
auto b = raw(del);
s.feed(b.data(), b.size());
CHECK(err.find("packet handler rejected") != std::string::npos,
"rejected packet -> explicit on_error");
CHECK(s.state() == ClassicStream::State::Offline, "rejected packet -> disconnect");
}
// ---- GC_KEY_AGREEMENT: a partial packet just waits (no error, no crash) ----
// (the full DH2 exchange lives in net_classic_encstream_test.)
{
ClassicStream s;
std::string err;
s.on_error = [&](const std::string &e) { err = e; };
uint8_t ka[100] = {HDR_KEY_AGREEMENT}; // < sizeof(KeyAgreement) == 261
s.feed(ka, sizeof(ka));
s.feed(ka, sizeof(ka)); // still short (200 < 261)
CHECK(err.empty(), "partial GC_KEY_AGREEMENT -> buffered, no error");
}
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
std::puts("net_classic_stream_test OK");
return 0;
}