Implement 40250 classic client port
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ---
|
||||
{
|
||||
|
||||
@@ -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(); });
|
||||
|
||||
Reference in New Issue
Block a user