Implement 40250 classic client port

This commit is contained in:
shenlei
2026-09-01 18:49:28 +09:00
parent 2277b1dd60
commit 8f61990a83
481 changed files with 169826 additions and 293 deletions
+104
View File
@@ -0,0 +1,104 @@
// net_classic_cipher_test — 40250 _IMPROVED_PACKET_ENCRYPTION_ end to end,
// no socket: a client-polarity ClassicCipher and a server-polarity one run the
// DH2 key agreement against each other, then encrypt/decrypt must round-trip
// symmetrically (client.encrypt -> server.decrypt and vice versa).
#include "../src/net/classic/classic_cipher.h"
#include "../src/net/classic/wire_classic.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)
int main() {
ClassicCipher client; // polarity = true (EterLib/NetStream.cpp:3004)
ClassicCipher server; // polarity = false (desc.cpp FinishHandshake)
// Each side generates its DH2 public blob (spub || epub).
uint8_t cbuf[KEY_AGREEMENT_MAX_DATA_LEN];
uint8_t sbuf[KEY_AGREEMENT_MAX_DATA_LEN];
size_t clen = sizeof(cbuf);
size_t slen = sizeof(sbuf);
size_t cagreed = client.prepare(cbuf, &clen);
size_t sagreed = server.prepare(sbuf, &slen);
CHECK(cagreed != 0, "client prepare -> non-zero agreed length");
CHECK(sagreed != 0, "server prepare -> non-zero agreed length");
CHECK(cagreed == sagreed, "both sides same agreed-value length (same DH2 params)");
CHECK(clen > 0 && clen <= KEY_AGREEMENT_MAX_DATA_LEN, "client blob length sane");
CHECK(clen == slen, "blob lengths match");
// Activate: each consumes the PEER's (agreed_length, blob).
bool ca = client.activate(true, sagreed, sbuf, slen);
bool sa = server.activate(false, cagreed, cbuf, clen);
CHECK(ca, "client activate");
CHECK(sa, "server activate");
CHECK(client.key_ready() && server.key_ready(), "both have encoder+decoder");
// The stream turns real only after GC_KEY_AGREEMENT_COMPLETED.
CHECK(!client.activated(), "cipher not 'activated' until set_activated");
client.set_activated(true);
server.set_activated(true);
// --- round trip: client -> server ---
{
std::string plain = "the quick brown fox CG_MOVE \x07\x01\x00\x00 jumps";
std::vector<uint8_t> buf(plain.begin(), plain.end());
std::vector<uint8_t> orig = buf;
client.encrypt(buf.data(), buf.size());
CHECK(buf != orig, "ciphertext differs from plaintext");
server.decrypt(buf.data(), buf.size());
CHECK(buf == orig, "server recovers client's plaintext");
}
// --- round trip: server -> client, and CTR keystream continuity ---
{
std::string a = "GC_MAIN_CHARACTER hello world 0123456789";
std::string b = "second chunk keeps the CTR counter advancing";
std::vector<uint8_t> ba(a.begin(), a.end()), bb(b.begin(), b.end());
std::vector<uint8_t> oa = ba, ob = bb;
server.encrypt(ba.data(), ba.size());
server.encrypt(bb.data(), bb.size());
client.decrypt(ba.data(), ba.size());
client.decrypt(bb.data(), bb.size());
CHECK(ba == oa && bb == ob, "client recovers server's two chunks (CTR continuity)");
}
// --- a fresh pair must derive a *different* key (ephemeral) ---
{
ClassicCipher c2, s2;
uint8_t x[256], y[256];
size_t xl = sizeof(x), yl = sizeof(y);
size_t xa = c2.prepare(x, &xl);
size_t ya = s2.prepare(y, &yl);
c2.activate(true, ya, y, yl);
s2.activate(false, xa, x, xl);
c2.set_activated(true);
s2.set_activated(true);
std::string msg = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; // 32 identical bytes
std::vector<uint8_t> p1(msg.begin(), msg.end());
std::vector<uint8_t> p2 = p1;
client.encrypt(p1.data(), p1.size()); // from the first session (already advanced)
c2.encrypt(p2.data(), p2.size()); // fresh session
CHECK(p1 != p2, "different sessions produce different ciphertext");
}
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
std::puts("net_classic_cipher_test OK");
return 0;
}
@@ -0,0 +1,168 @@
// net_classic_encstream_test — ClassicStream with _IMPROVED_PACKET_ENCRYPTION_.
// A server-side ClassicCipher plays the peer: HANDSHAKE -> GC_KEY_AGREEMENT ->
// (stream replies CG_KEY_AGREEMENT) -> GC_KEY_AGREEMENT_COMPLETED. After that
// every byte both ways is CTR-encrypted; the test verifies the stream decrypts
// an inbound GC_PHASE and that an outbound CG_MOVE decrypts cleanly server-side.
#include "../src/net/classic/classic_cipher.h"
#include "../src/net/classic/classic_stream.h"
#include "../src/net/classic/wire_classic.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[2048];
for (size_t n; (n = s.take_outgoing(buf, sizeof(buf)));) {
out.insert(out.end(), buf, buf + n);
}
return out;
}
int main() {
ClassicStream cs;
cs.set_polarity(true); // client
int phase_seen = -1;
std::string err;
bool cipher_on = false;
cs.on_phase = [&](uint8_t p) { phase_seen = p; };
cs.on_error = [&](const std::string &e) { err = e; };
cs.on_cipher_active = [&]() { cipher_on = true; };
ClassicCipher server; // polarity = false
// --- handshake (plaintext) ---
{
Handshake hs{};
hs.header = HDR_HANDSHAKE;
hs.time = 1000;
hs.delta = 5;
cs.feed(raw(hs).data(), sizeof(hs));
CHECK(drain(cs).size() == sizeof(Handshake), "handshake echo (plaintext)");
}
// --- GC_KEY_AGREEMENT: server sends its DH2 blob ---
{
KeyAgreement srv_pkt{};
srv_pkt.header = HDR_KEY_AGREEMENT;
size_t dl = sizeof(srv_pkt.data);
size_t agreed = server.prepare(srv_pkt.data, &dl);
CHECK(agreed != 0, "server DH2 prepare");
srv_pkt.agreed_length = (uint16_t)agreed;
srv_pkt.data_length = (uint16_t)dl;
cs.feed(raw(srv_pkt).data(), sizeof(srv_pkt));
// stream must have replied with CG_KEY_AGREEMENT (plaintext, its own blob)
auto reply = drain(cs);
CHECK(reply.size() == sizeof(KeyAgreement), "CG_KEY_AGREEMENT reply is 261 bytes");
CHECK(reply[0] == HDR_KEY_AGREEMENT, "reply header 0xFB");
KeyAgreement cli_pkt{};
std::memcpy(&cli_pkt, reply.data(), sizeof(cli_pkt));
CHECK(err.empty(), "no error during key agreement");
// server consumes the client's blob
CHECK(server.activate(false, cli_pkt.agreed_length, cli_pkt.data, cli_pkt.data_length),
"server activate with client blob");
CHECK(!cs.cipher_active(), "stream cipher not active until COMPLETED");
}
// --- GC_KEY_AGREEMENT_COMPLETED (plaintext) -> both sides go live ---
{
KeyAgreementCompleted done{};
done.header = HDR_GC_KEY_AGREEMENT_COMPLETED;
cs.feed(raw(done).data(), sizeof(done));
server.set_activated(true);
CHECK(cs.cipher_active() && cipher_on, "stream cipher active after COMPLETED");
}
// --- server -> client: an ENCRYPTED GC_PHASE(GAME) must decrypt + dispatch ---
{
auto pkt = raw(Phase_{HDR_GC_PHASE, PHASE_GAME});
server.encrypt(pkt.data(), pkt.size());
CHECK(pkt[0] != HDR_GC_PHASE, "GC_PHASE really is ciphertext on the wire");
cs.feed(pkt.data(), pkt.size());
CHECK(phase_seen == PHASE_GAME, "stream decrypted + dispatched GC_PHASE(GAME)");
}
// --- client -> server: an outbound CG_MOVE must be ciphertext the server reads ---
{
cs.set_sequence_mode(true);
CGMove mv{};
mv.header = HDR_CG_MOVE;
mv.func = 1;
mv.x = 12345;
mv.y = 67890;
cs.send_fixed(&mv, sizeof(mv));
auto wire = drain(cs);
CHECK(wire.size() == sizeof(CGMove) + 1, "CG_MOVE + seq byte on the wire");
CHECK(wire[0] != HDR_CG_MOVE, "CG_MOVE header is encrypted on the wire");
server.decrypt(wire.data(), wire.size());
CGMove got{};
std::memcpy(&got, wire.data(), sizeof(got));
CHECK(got.header == HDR_CG_MOVE && got.func == 1 && got.x == 12345 && got.y == 67890,
"server decrypts CG_MOVE body");
CHECK(wire[sizeof(CGMove)] == SEQUENCE_TABLE[0], "seq byte decrypts to table[0]");
}
// --- pipelined case: COMPLETED + encrypted packet in one feed() ---
{
ClassicStream cs2;
cs2.set_polarity(true);
int seen2 = -1;
cs2.on_phase = [&](uint8_t p) { seen2 = p; };
ClassicCipher srv2;
Handshake hs{};
hs.header = HDR_HANDSHAKE;
cs2.feed(raw(hs).data(), sizeof(hs));
drain(cs2);
KeyAgreement sp{};
sp.header = HDR_KEY_AGREEMENT;
size_t dl = sizeof(sp.data);
sp.agreed_length = (uint16_t)srv2.prepare(sp.data, &dl);
sp.data_length = (uint16_t)dl;
cs2.feed(raw(sp).data(), sizeof(sp));
auto rep = drain(cs2);
KeyAgreement cp{};
std::memcpy(&cp, rep.data(), sizeof(cp));
srv2.activate(false, cp.agreed_length, cp.data, cp.data_length);
srv2.set_activated(true);
// one buffer: [COMPLETED plaintext][encrypted GC_PHASE(SELECT)]
std::vector<uint8_t> blob = raw(KeyAgreementCompleted{HDR_GC_KEY_AGREEMENT_COMPLETED, {}});
auto enc = raw(Phase_{HDR_GC_PHASE, PHASE_SELECT});
srv2.encrypt(enc.data(), enc.size());
blob.insert(blob.end(), enc.begin(), enc.end());
cs2.feed(blob.data(), blob.size());
CHECK(seen2 == PHASE_SELECT, "pipelined COMPLETED+encrypted packet handled");
}
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
std::puts("net_classic_encstream_test OK");
return 0;
}
+145
View File
@@ -0,0 +1,145 @@
#include "classic/classic_mark_client.h"
#include <cassert>
#include <cstdint>
#include <cstring>
#include <vector>
using namespace mtnet::classic;
static std::vector<uint8_t> take_all(ClassicStream &stream) {
std::vector<uint8_t> out(stream.outgoing_pending());
if (!out.empty()) {
assert(stream.take_outgoing(out.data(), out.size()) == out.size());
}
return out;
}
static void put_u16(std::vector<uint8_t> &p, size_t off, uint16_t v) {
p[off] = static_cast<uint8_t>(v);
p[off + 1] = static_cast<uint8_t>(v >> 8);
}
static void put_u32(std::vector<uint8_t> &p, size_t off, uint32_t v) {
p[off] = static_cast<uint8_t>(v);
p[off + 1] = static_cast<uint8_t>(v >> 8);
p[off + 2] = static_cast<uint8_t>(v >> 16);
p[off + 3] = static_cast<uint8_t>(v >> 24);
}
static void test_mark_download() {
ClassicMarkClient client(0x11223344u, 0x55667788u);
Handshake handshake{};
handshake.header = HDR_HANDSHAKE;
handshake.handshake = 7;
handshake.time = 100;
client.stream().feed(&handshake, sizeof(handshake));
std::vector<uint8_t> login = take_all(client.stream());
assert(login.size() == sizeof(Handshake) + sizeof(CGMarkLogin));
assert(login[sizeof(Handshake)] == HDR_CG_MARK_LOGIN);
uint32_t handle = 0;
std::memcpy(&handle, login.data() + sizeof(Handshake) + 1, sizeof(handle));
assert(handle == 0x11223344u);
uint32_t random_key = 0;
std::memcpy(&random_key, login.data() + sizeof(Handshake) + 5, sizeof(random_key));
assert(random_key == 0x55667788u);
Handshake resync = handshake;
resync.time = 200;
resync.delta = 3;
client.stream().feed(&resync, sizeof(resync));
std::vector<uint8_t> repeated_login = take_all(client.stream());
assert(repeated_login.size() == sizeof(Handshake) + sizeof(CGMarkLogin));
assert(repeated_login[0] == HDR_HANDSHAKE);
assert(repeated_login[sizeof(Handshake)] == HDR_CG_MARK_LOGIN);
std::vector<uint8_t> phase{HDR_GC_PHASE, PHASE_LOGIN};
client.stream().feed(phase.data(), phase.size());
std::vector<uint8_t> request = take_all(client.stream());
assert(request.size() == sizeof(CGMarkIDXList));
assert(request[0] == HDR_CG_MARK_IDXLIST);
// One guild maps to image zero, position one. Feed the whole-size frame in
// two chunks to exercise the custom raw framing path.
std::vector<uint8_t> idx(sizeof(GCMarkIDXList) + 4);
idx[0] = HDR_GC_MARK_IDXLIST;
put_u32(idx, 1, static_cast<uint32_t>(idx.size()));
put_u16(idx, 5, 1);
put_u16(idx, 7, 42);
put_u16(idx, 9, 1);
client.stream().feed(idx.data(), 6);
assert(client.stream().outgoing_pending() == 0);
client.stream().feed(idx.data() + 6, idx.size() - 6);
request = take_all(client.stream());
assert(request.size() == sizeof(CGMarkCRCList));
assert(request[0] == HDR_CG_MARK_CRCLIST && request[1] == 0);
assert(client.marks().has_mark(42));
// A valid zero-block image response advances the image state and completes
// the download without requiring LZO data in this framing test.
std::vector<uint8_t> block(sizeof(GCMarkBlock));
block[0] = HDR_GC_MARK_BLOCK;
put_u32(block, 1, static_cast<uint32_t>(block.size()));
block[5] = 0;
put_u32(block, 6, 0);
client.stream().feed(block.data(), block.size());
assert(client.complete());
}
static void test_symbol_download() {
ClassicMarkClient client(0x11u, 0x22u);
client.set_download_symbol(42);
std::vector<uint8_t> phase{HDR_GC_PHASE, PHASE_LOGIN};
client.stream().feed(phase.data(), phase.size());
std::vector<uint8_t> request = take_all(client.stream());
assert(request.size() == sizeof(CGSymbolCRC));
assert(request[0] == HDR_CG_SYMBOL_CRC);
uint32_t gid = 0;
std::memcpy(&gid, request.data() + 1, sizeof(gid));
assert(gid == 42);
const std::vector<uint8_t> payload{0xAA, 0xBB, 0xCC};
std::vector<uint8_t> symbol(sizeof(GCSymbolData) + payload.size());
symbol[0] = HDR_GC_SYMBOL_DATA;
put_u16(symbol, 1, static_cast<uint16_t>(symbol.size()));
put_u32(symbol, 3, 42);
std::memcpy(symbol.data() + sizeof(GCSymbolData), payload.data(), payload.size());
client.stream().feed(symbol.data(), symbol.size());
assert(client.complete());
assert(client.symbol_guild_id() == 42);
assert(client.symbol_data() == payload);
}
static void test_upload_layouts() {
uint32_t px[GUILD_MARK_WIDTH * GUILD_MARK_HEIGHT] = {};
px[0] = 0x44332211u;
ClassicMarkClient mark(1, 2);
mark.set_upload_mark(42, px);
std::vector<uint8_t> phase{HDR_GC_PHASE, PHASE_LOGIN};
mark.stream().feed(phase.data(), phase.size());
std::vector<uint8_t> packet = take_all(mark.stream());
assert(packet.size() == sizeof(CGMarkUpload));
assert(packet[0] == HDR_CG_MARK_UPLOAD);
uint32_t gid = 0;
std::memcpy(&gid, packet.data() + 1, sizeof(gid));
assert(gid == 42);
assert(packet[5] == 0x11 && packet[6] == 0x22 && packet[7] == 0x33 && packet[8] == 0x44);
ClassicMarkClient symbol(3, 4);
symbol.set_upload_symbol(99, std::vector<uint8_t>{1, 2, 3});
symbol.stream().feed(phase.data(), phase.size());
packet = take_all(symbol.stream());
assert(packet.size() == sizeof(CGGuildSymbolUpload) + 3);
assert(packet[0] == HDR_CG_GUILD_SYMBOL_UPLOAD);
assert(packet[1] == packet.size() && packet[2] == 0);
std::memcpy(&gid, packet.data() + 3, sizeof(gid));
assert(gid == 99);
assert(packet[7] == 1 && packet[8] == 2 && packet[9] == 3);
}
int main() {
test_mark_download();
test_symbol_download();
test_upload_layouts();
return 0;
}
File diff suppressed because it is too large Load Diff
+241
View File
@@ -0,0 +1,241 @@
// 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");
}
// ---------------------------------------------------------- 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");
}
// ---- 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;
}
+153
View File
@@ -0,0 +1,153 @@
// net_classic_wire_test — 40250 "classic" backend wire layer.
// The struct sizes are already guarded by static_assert in wire_classic.h (so a
// mismatch is a compile error); this test locks down the size-table / sequence /
// dynamic-flag lookups and a couple of round-trip byte layouts.
#include "../src/net/classic/wire_classic.h"
#include <cstdio>
#include <cstring>
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)
int main() {
// --- header values (spot check vs server packet.h) ---
CHECK(HDR_CG_MOVE == 7, "CG_MOVE == 7");
CHECK(HDR_GC_MAIN_CHARACTER == 113, "GC_MAIN_CHARACTER == 113");
CHECK(HDR_GC_LOGIN_SUCCESS_NEWSLOT == 32, "GC_LOGIN_SUCCESS_NEWSLOT == 32");
CHECK(HDR_HANDSHAKE == 0xFF, "HANDSHAKE == 0xFF");
CHECK(HDR_GC_TIME_SYNC == 0xFC && HDR_CG_TIME_SYNC == 0xFC, "TIME_SYNC == 0xFC");
CHECK(HDR_KEY_AGREEMENT == 0xFB, "KEY_AGREEMENT == 0xFB");
CHECK(PHASE_GAME == 5 && PHASE_SELECT == 3, "EPhase ordinals");
// --- struct sizes (redundant with static_assert, but explicit) ---
CHECK(sizeof(Handshake) == 13, "sizeof Handshake");
CHECK(sizeof(CGText) == 1, "sizeof CG_TEXT base");
CHECK(sizeof(SimplePlayer) == 63, "sizeof SimplePlayer");
CHECK(sizeof(GCMainCharacter) == 46, "sizeof GCMainCharacter");
CHECK(sizeof(GCMainCharacter3BGM) == 71 && sizeof(GCMainCharacter4BGM) == 75,
"sizeof main-character BGM variants");
CHECK(sizeof(GCCharacterAdd2) == 80 && sizeof(GCCharacterUpdate) == 35,
"sizeof character add/update variants");
CHECK(sizeof(GCFishing) == 7, "sizeof GC_FISHING");
CHECK(sizeof(GCWarpClassic) == 15, "sizeof classic GC_WARP");
CHECK(sizeof(GCAuthSuccess) == 6 && sizeof(GCAuthSuccessOpenID) == 37,
"sizeof auth success packets");
CHECK(sizeof(GCPasspodFailed) == 130 && sizeof(GCXTrapRequest) == 129 && sizeof(GCHSRequest) == 403,
"sizeof optional security packets");
CHECK(sizeof(GCLoginSuccess) == 329, "sizeof GCLoginSuccess (NEWSLOT, 4 players)");
CHECK(sizeof(GCPoints) == 1 + 255 * 4, "sizeof GCPoints");
CHECK(sizeof(GCPointChange) == 17, "sizeof GCPointChange (int header quirk)");
CHECK(sizeof(CGMove) == 16, "sizeof CGMove");
CHECK(sizeof(GCMove) == 24, "sizeof GCMove");
CHECK(sizeof(CGMessenger) == 2 && sizeof(CGExchange) == 10, "sizeof P8 CG packets");
CHECK(sizeof(GCExchange) == 47, "sizeof GC_EXCHANGE");
CHECK(sizeof(GCMessengerHead) == 4 && sizeof(GCSafeboxMoneyChange) == 5,
"sizeof P8 dynamic/storage packets");
// --- size-table lookup (body size, no trailing seq byte) ---
CHECK(packet_size_gc(HDR_GC_PHASE) == 2, "gc size PHASE");
CHECK(packet_size_gc(HDR_GC_MAIN_CHARACTER) == 46, "gc size MAIN_CHARACTER");
CHECK(packet_size_gc(HDR_GC_WARP) == sizeof(GCWarpClassic), "gc size WARP");
CHECK(packet_size_gc(HDR_GC_MAIN_CHARACTER3_BGM) == 71 &&
packet_size_gc(HDR_GC_MAIN_CHARACTER4_BGM_VOL) == 75, "gc size MAIN_CHARACTER BGM");
CHECK(packet_size_gc(HDR_GC_CHARACTER_ADD2) == 80 &&
packet_size_gc(HDR_GC_CHARACTER_UPDATE2) == 35, "gc size character variants");
CHECK(packet_size_gc(HDR_GC_FISHING) == 7, "gc size FISHING");
CHECK(packet_size_gc(HDR_GC_AUTH_SUCCESS) == 6 &&
packet_size_gc(HDR_GC_XTRAP_CS1_REQUEST) == 129, "gc size auth/security");
CHECK(packet_size_gc(HDR_GC_LOGIN_SUCCESS_NEWSLOT) == 329, "gc size LOGIN_SUCCESS_NEWSLOT");
CHECK(packet_size_gc(HDR_GC_CHARACTER_POINTS) == 1021, "gc size CHARACTER_POINTS");
CHECK(packet_size_gc(HDR_GC_EXCHANGE) == 47, "gc size EXCHANGE");
CHECK(packet_size_gc(HDR_GC_SAFEBOX_MONEY_CHANGE) == 5, "gc size SAFEBOX_MONEY_CHANGE");
CHECK(packet_size_gc(HDR_GC_SAFEBOX_SET) == 51, "gc size SAFEBOX_SET");
CHECK(packet_size_gc(HDR_GC_MALL_OPEN) == 2, "gc size MALL_OPEN");
CHECK(packet_size_gc(HDR_GC_CHAT) == 0, "gc size CHAT == 0 (dynamic)");
CHECK(packet_size_cg(HDR_CG_LOGIN) == 49, "cg size LOGIN");
CHECK(packet_size_cg(HDR_CG_TEXT) == sizeof(CGText), "cg size TEXT base");
CHECK(packet_size_cg(HDR_CG_MOVE) == 16, "cg size MOVE");
CHECK(packet_size_cg(HDR_CG_ENTERGAME) == 1, "cg size ENTERGAME");
CHECK(packet_size_cg(HDR_CG_EXCHANGE) == 10, "cg size EXCHANGE");
CHECK(packet_size_cg(HDR_CG_MESSENGER) == 2, "cg size MESSENGER base");
CHECK(packet_size_cg(HDR_CG_SAFEBOX_ITEM_MOVE) == 8, "cg size SAFEBOX_ITEM_MOVE");
// --- sequence-byte flags (docs §2.2) ---
CHECK(is_sequence_cg(HDR_CG_MOVE), "CG_MOVE has sequence");
CHECK(is_sequence_cg(HDR_CG_LOGIN), "CG_LOGIN has sequence");
CHECK(is_sequence_cg(HDR_CG_TIME_SYNC), "CG_TIME_SYNC has sequence");
CHECK(!is_sequence_cg(HDR_HANDSHAKE), "HANDSHAKE no sequence");
CHECK(!is_sequence_cg(HDR_KEY_AGREEMENT), "KEY_AGREEMENT no sequence");
CHECK(!is_sequence_cg(HDR_CG_MARK_UPLOAD), "MARK_UPLOAD no sequence");
CHECK(!is_sequence_cg(HDR_CG_DRAGON_SOUL_REFINE), "DRAGON_SOUL_REFINE no sequence");
CHECK(!is_sequence_cg(HDR_CG_STATE_CHECKER), "STATE_CHECKER no sequence");
// --- dynamic-size flags (docs §2.2) ---
CHECK(is_dynamic_gc(HDR_GC_CHAT), "GC_CHAT dynamic");
CHECK(is_dynamic_gc(HDR_GC_SHOP), "GC_SHOP dynamic");
CHECK(is_dynamic_gc(HDR_GC_GUILD), "GC_GUILD dynamic");
CHECK(is_dynamic_gc(HDR_GC_QUEST_INFO), "GC_QUEST_INFO dynamic");
CHECK(!is_dynamic_gc(HDR_GC_MALL_SET), "GC_MALL_SET NOT dynamic");
CHECK(!is_dynamic_gc(HDR_GC_SAFEBOX_SET), "GC_SAFEBOX_SET NOT dynamic");
CHECK(!is_dynamic_gc(HDR_GC_PARTY_ADD), "GC_PARTY_ADD NOT dynamic");
// client map registers GC_WHISPER STATIC, but its wSize sits where
// TDynamicSizePacketHeader.size does — we frame it via the dynamic path.
CHECK(is_dynamic_gc(HDR_GC_WHISPER), "GC_WHISPER framed as dynamic (wSize prefix)");
CHECK(is_dynamic_cg(HDR_CG_CHAT) && is_dynamic_cg(HDR_CG_WHISPER), "CG chat/whisper dynamic");
CHECK(!is_dynamic_cg(HDR_CG_SHOP), "CG_SHOP NOT dynamic");
// --- byte-layout round trip: GC_MAIN_CHARACTER ---
{
GCMainCharacter mc{};
mc.header = HDR_GC_MAIN_CHARACTER;
mc.vid = 0x11223344;
mc.race = 4;
std::strcpy(mc.name, "Hero");
mc.x = 100000;
mc.y = 200000;
mc.z = 12;
mc.empire = 2;
mc.skill_group = 1;
uint8_t buf[sizeof(mc)];
std::memcpy(buf, &mc, sizeof(mc));
CHECK(buf[0] == 113, "wire byte 0 == header");
// header(1) vid(4) race(2) name(25) -> x at offset 32
int32_t x_wire;
std::memcpy(&x_wire, buf + 1 + 4 + 2 + 25, 4);
CHECK(x_wire == 100000, "wire x at offset 32");
GCMainCharacter rt{};
std::memcpy(&rt, buf, sizeof(rt));
CHECK(rt.vid == 0x11223344 && rt.z == 12 && rt.empire == 2, "round-trip fields");
CHECK(std::strcmp(rt.name, "Hero") == 0, "round-trip name");
}
// --- GCPointChange: real header byte is buf[0] despite `int32_t header` ---
{
GCPointChange pc{};
pc.header = HDR_GC_CHARACTER_POINT_CHANGE; // 17
pc.vid = 7;
pc.type = 5;
pc.amount = -3;
pc.value = 42;
uint8_t buf[sizeof(pc)];
std::memcpy(buf, &pc, sizeof(pc));
CHECK(buf[0] == 17, "point_change wire byte 0 == 17");
CHECK(sizeof(pc) == 17, "point_change wire size 17 (4B int header + 4+1+4+4)");
}
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;
}
std::puts("net_classic_wire_test OK");
return 0;
}
+127
View File
@@ -825,6 +825,20 @@ int main() {
if (f.name == "Bob") bob_now = f.online;
}
CHECK(bob_now, "messenger: LOGIN flips Bob online");
const char *invite = "messenger_auth Alice";
std::vector<uint8_t> invite_buf(sizeof(GCChat) + std::strlen(invite) + 1);
GCChat invite_head{};
invite_head.header = GC_CHAT;
invite_head.length = (uint16_t)invite_buf.size();
invite_head.type = CHAT_TYPE_COMMAND;
std::memcpy(invite_buf.data(), &invite_head, sizeof(invite_head));
std::memcpy(invite_buf.data() + sizeof(invite_head), invite, std::strlen(invite) + 1);
es.apply(GC_CHAT, invite_buf.data(), (uint16_t)invite_buf.size());
auto invites = es.drain_friend_invites();
CHECK(invites.size() == 1 && invites[0] == "Alice",
"messenger: invitation command surfaced");
CHECK(es.drain_friend_invites().empty(), "messenger: invitation queue drained");
}
// --- P8 NPC shop (GC_SHOP START / END + error) ---
@@ -1414,6 +1428,119 @@ int main() {
"cube: COMMAND lines never enter the chat log");
}
// --- 40250 command-bus callbacks outside cube -------------------------
{
auto cmd = [&](const std::string &text) {
std::vector<uint8_t> buf(sizeof(GCChat) + text.size() + 1);
GCChat h{};
h.header = GC_CHAT;
h.length = (uint16_t)buf.size();
h.type = CHAT_TYPE_COMMAND;
std::memcpy(buf.data(), &h, sizeof(h));
std::memcpy(buf.data() + sizeof(h), text.c_str(), text.size() + 1);
es.apply(GC_CHAT, buf.data(), (uint16_t)buf.size());
};
cmd("PartyRequestDenied");
cmd("ShowMeSafeboxPassword");
cmd("ShowMeMallPassword");
cmd("RefineSuceeded");
cmd("RefineFailed");
cmd("OpenPrivateShop");
cmd("MyShopPriceList 11209 250000");
cmd("setblockmode 37");
cmd("ObserverMode 1");
cmd("ObserverCount 3");
cmd("StoneDetect 9001 2 90");
cmd("StartStaminaConsume 10 77");
cmd("StopStaminaConsume 55");
cmd("sms");
cmd("combo 1");
cmd("mobile_auth");
cmd("gift");
cmd("kiss 1 2");
cmd("slap 1 2");
cmd("dance6 1");
cmd("joy 2");
auto events = es.drain_server_commands();
int denied = 0, safe = 0, mall = 0, ok = 0, fail = 0, open = 0, price = 0, block = 0;
int observer = 0, observer_count = 0, stone = 0, stamina_start = 0, stamina_stop = 0;
int mobile = 0, mobile_auth = 0, combo = 0, gift = 0;
for (const auto &ev : events) {
switch (ev.kind) {
case ServerCommandEvent::PartyRequestDenied: ++denied; break;
case ServerCommandEvent::SafeboxPasswordRequired: ++safe; break;
case ServerCommandEvent::SafeboxWrongPassword: break;
case ServerCommandEvent::MallPasswordRequired: ++mall; break;
case ServerCommandEvent::RefineSucceeded: ++ok; break;
case ServerCommandEvent::RefineFailed: ++fail; break;
case ServerCommandEvent::PrivateShopOpenRequested: ++open; break;
case ServerCommandEvent::MyShopPrice:
price += ev.value == 11209 && ev.value2 == 250000;
break;
case ServerCommandEvent::BlockModeChanged:
block += ev.value == 37;
break;
case ServerCommandEvent::ObserverModeChanged:
observer += ev.value == 1 && es.observer_mode();
break;
case ServerCommandEvent::ObserverCountChanged:
observer_count += ev.value == 3 && es.observer_count() == 3;
break;
case ServerCommandEvent::StoneDetected:
stone += ev.value == 9001 && ev.value2 == 2 && ev.value3 == 90.0f;
break;
case ServerCommandEvent::StaminaStarted:
stamina_start += ev.value == 10 && ev.value2 == 77;
break;
case ServerCommandEvent::StaminaStopped:
stamina_stop += ev.value == 0 && ev.value2 == 55;
break;
case ServerCommandEvent::MobileFlagChanged:
mobile += ev.value == 1 && es.mobile_flag();
break;
case ServerCommandEvent::MobileAuthRequired:
++mobile_auth;
break;
case ServerCommandEvent::ComboChanged:
combo += ev.value == 1 && es.combo_skill_flag();
break;
case ServerCommandEvent::GiftAvailable:
++gift;
break;
}
}
CHECK(denied == 1 && safe == 1 && mall == 1 && ok == 1 && fail == 1 && open == 1
&& price == 1 && block == 1 && observer == 1 && observer_count == 1
&& stone == 1 && stamina_start == 1 && stamina_stop == 1 && mobile == 1
&& mobile_auth == 1 && combo == 1 && gift == 1 && !es.stamina_consuming()
&& es.current_stamina() == 55,
"40250 command bus: callbacks surfaced");
auto motions = es.drain_motions();
CHECK(motions.size() == 6 && motions[0].vid == 1 && motions[0].victim_vid == 2
&& motions[0].motion == 308 && motions[1].vid == 2 && motions[1].motion == 308
&& motions[2].motion == 320 && motions[3].motion == 316
&& motions[4].motion == 330 && motions[5].motion == 349,
"40250 command bus: emotions feed the GC_MOTION stream");
es.set_now(1000);
cmd("StartStaminaConsume 10 77");
es.drain_server_commands();
es.set_now(1500);
es.tick();
CHECK(es.stamina_consuming() && es.current_stamina() == 72
&& es.points().stamina() == 72 && es.take_points_dirty(),
"40250 command bus: stamina drains at server-command rate");
es.mut_safebox_wrong_password();
bool wrong_password = false;
for (const auto &ev : es.drain_server_commands()) {
wrong_password = wrong_password || ev.kind == ServerCommandEvent::SafeboxWrongPassword;
}
CHECK(wrong_password, "40250 command bus: wrong safebox password surfaced");
cmd("CloseSafebox");
cmd("CloseMall");
CHECK(!es.safebox_open() && !es.mall_open(),
"40250 command bus: storage close callbacks update state");
}
// --- despawn ---
// --- guild land list + minimap observers ---
{
+17 -3
View File
@@ -47,16 +47,26 @@ public:
uint16_t listen_ephemeral() {
m_listen = ::socket(AF_INET, SOCK_STREAM, 0);
if (m_listen < 0) {
return 0;
}
int one = 1;
setsockopt(m_listen, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
sockaddr_in a{};
a.sin_family = AF_INET;
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
a.sin_port = 0;
bind(m_listen, (sockaddr *)&a, sizeof(a));
::listen(m_listen, 1);
if (::bind(m_listen, (sockaddr *)&a, sizeof(a)) != 0 || ::listen(m_listen, 1) != 0) {
::close(m_listen);
m_listen = -1;
return 0;
}
socklen_t l = sizeof(a);
getsockname(m_listen, (sockaddr *)&a, &l);
if (::getsockname(m_listen, (sockaddr *)&a, &l) != 0) {
::close(m_listen);
m_listen = -1;
return 0;
}
int fl = fcntl(m_listen, F_GETFL, 0);
fcntl(m_listen, F_SETFL, fl | O_NONBLOCK);
return ntohs(a.sin_port);
@@ -694,6 +704,10 @@ int main() {
{
MockServer srv(MockServer::Mode::Auth);
uint16_t port = srv.listen_ephemeral();
if (port == 0) {
std::fprintf(stdout, "SKIP: localhost socket bind unavailable in this environment\n");
return 0;
}
AuthClient auth("admin", "123456789");
CHECK(auth.connect("127.0.0.1", port), "auth connect");
run(srv, auth, 5000, [&] { return auth.done(); });