1086 lines
40 KiB
C++
1086 lines
40 KiB
C++
// net_classic_session_test — 40250 classic HANDSHAKE -> LOGIN -> SELECT ->
|
|
// LOADING -> GAME driven socket-free: scripted server bytes into
|
|
// session.stream().feed(), outgoing checked via take_outgoing().
|
|
#include "../src/net/classic/classic_session.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 void feed(ClassicSession &s, const std::vector<uint8_t> &b) {
|
|
s.stream().feed(b.data(), b.size());
|
|
}
|
|
|
|
static std::vector<uint8_t> drain(ClassicSession &s) {
|
|
std::vector<uint8_t> out;
|
|
uint8_t buf[2048];
|
|
for (;;) {
|
|
size_t n = s.stream().take_outgoing(buf, sizeof(buf));
|
|
if (!n) {
|
|
break;
|
|
}
|
|
out.insert(out.end(), buf, buf + n);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
int main() {
|
|
ClassicSession s;
|
|
s.set_now(0);
|
|
|
|
std::vector<CharSlot> got_list;
|
|
bool entered = false;
|
|
s.on_char_list = [&](const std::vector<CharSlot> &l) { got_list = l; };
|
|
s.on_entered_game = [&]() { entered = true; };
|
|
|
|
s.start_offline("admin", "123456789");
|
|
CHECK(s.stage() == mtnet::INetSession::Stage::Connecting, "start_offline -> Connecting");
|
|
|
|
// --- handshake: server HANDSHAKE -> client echoes it back ---
|
|
{
|
|
Handshake hs{};
|
|
hs.header = HDR_HANDSHAKE;
|
|
hs.handshake = 0x1234;
|
|
hs.time = 5000;
|
|
hs.delta = 10;
|
|
feed(s, raw(hs));
|
|
auto out = drain(s);
|
|
CHECK(out.size() == sizeof(Handshake), "handshake echo (13 bytes, no seq)");
|
|
Handshake e{};
|
|
std::memcpy(&e, out.data(), sizeof(e));
|
|
CHECK(e.header == HDR_HANDSHAKE && e.time == 5000 + 20 && e.delta == 0, "handshake echo math");
|
|
}
|
|
|
|
// --- GC_PHASE(LOGIN): session sends CG_LOGIN + first sequence byte ---
|
|
{
|
|
Phase_ p{HDR_GC_PHASE, PHASE_LOGIN};
|
|
feed(s, raw(p));
|
|
CHECK(s.stage() == mtnet::INetSession::Stage::LoggingIn, "PHASE_LOGIN -> LoggingIn");
|
|
auto out = drain(s);
|
|
CHECK(out.size() == sizeof(CGLogin) + 1, "CG_LOGIN + 1 seq byte");
|
|
CHECK(out[0] == HDR_CG_LOGIN, "outgoing is CG_LOGIN");
|
|
CGLogin lg{};
|
|
std::memcpy(&lg, out.data(), sizeof(lg));
|
|
CHECK(std::strcmp(lg.login, "admin") == 0, "login field");
|
|
CHECK(std::strcmp(lg.passwd, "123456789") == 0, "passwd field");
|
|
CHECK(out[sizeof(CGLogin)] == SEQUENCE_TABLE[0], "trailing seq == table[0]");
|
|
CHECK(s.stream().sequence_index() == 1, "seq index -> 1");
|
|
}
|
|
|
|
// --- GC_LOGIN_SUCCESS_NEWSLOT (header 32, 4 slots; 2 filled) ---
|
|
{
|
|
GCLoginSuccess ls{};
|
|
ls.header = HDR_GC_LOGIN_SUCCESS_NEWSLOT;
|
|
ls.handle = 0xAABBCCDD;
|
|
ls.random_key = 0x99887766;
|
|
ls.players[0].id = 1001;
|
|
std::strcpy(ls.players[0].name, "Warrior");
|
|
ls.players[0].job = 0;
|
|
ls.players[0].level = 42;
|
|
ls.players[0].ht = 20;
|
|
ls.players[0].change_name = 1;
|
|
ls.players[1].id = 1002;
|
|
std::strcpy(ls.players[1].name, "Sura");
|
|
ls.players[1].job = 4;
|
|
ls.players[1].level = 7;
|
|
feed(s, raw(ls));
|
|
CHECK(s.parser().char_list_ready(), "login success parsed");
|
|
CHECK(s.parser().slot_count() == 4, "4 slots");
|
|
CHECK(s.parser().handle() == 0xAABBCCDD, "handle carried");
|
|
CHECK(s.parser().random_key() == 0x99887766, "random_key carried");
|
|
const auto &sl = s.char_slots();
|
|
CHECK(sl.size() == 4 && sl[0].name == "Warrior" && sl[0].level == 42 && sl[0].change_name,
|
|
"slot 0 + forced rename flag");
|
|
CHECK(sl[1].name == "Sura" && sl[1].job == 4, "slot 1");
|
|
CHECK(sl[2].empty() && sl[3].empty(), "slots 2,3 empty");
|
|
}
|
|
|
|
// --- GC_CHANGE_NAME completes the forced rename state ---
|
|
{
|
|
GCChangeNameNotice n{};
|
|
n.header = HDR_GC_CHANGE_NAME;
|
|
n.pid = 1001;
|
|
std::strcpy(n.name, "Renamed");
|
|
feed(s, raw(n));
|
|
const auto &sl = s.char_slots();
|
|
CHECK(sl[0].name == "Renamed" && !sl[0].change_name, "rename clears forced flag");
|
|
auto names = s.parser().drain_name_events();
|
|
CHECK(names.size() == 1 && names[0].pid == 1001 && names[0].name == "Renamed",
|
|
"rename event surfaced");
|
|
}
|
|
|
|
// --- GC_PHASE(SELECT): char list surfaces ---
|
|
{
|
|
Phase_ p{HDR_GC_PHASE, PHASE_SELECT};
|
|
feed(s, raw(p));
|
|
CHECK(s.stage() == mtnet::INetSession::Stage::CharSelect, "PHASE_SELECT -> CharSelect");
|
|
CHECK(got_list.size() == 4 && got_list[0].name == "Renamed" && !got_list[0].change_name,
|
|
"on_char_list fired with renamed slot");
|
|
}
|
|
|
|
// --- select_char(0) -> CG_CHARACTER_SELECT + seq[1] ---
|
|
{
|
|
CHECK(s.select_char(0), "select_char(0) ok");
|
|
CHECK(!s.select_char(9), "select_char(9) rejected (out of range)");
|
|
auto out = drain(s);
|
|
CHECK(out.size() == sizeof(CGPlayerSelect) + 1, "CG_CHARACTER_SELECT + seq");
|
|
CHECK(out[0] == HDR_CG_CHARACTER_SELECT && out[1] == 0, "select slot 0");
|
|
CHECK(out[sizeof(CGPlayerSelect)] == SEQUENCE_TABLE[1], "seq == table[1]");
|
|
}
|
|
|
|
// --- GC_PHASE(LOADING) + GC_MAIN_CHARACTER(113) + a nearby NPC ---
|
|
{
|
|
Phase_ p{HDR_GC_PHASE, PHASE_LOADING};
|
|
feed(s, raw(p));
|
|
CHECK(s.stage() == mtnet::INetSession::Stage::Loading, "PHASE_LOADING -> Loading");
|
|
|
|
GCMainCharacter mc{};
|
|
mc.header = HDR_GC_MAIN_CHARACTER;
|
|
mc.vid = 7777;
|
|
mc.race = 4;
|
|
std::strcpy(mc.name, "Warrior");
|
|
mc.x = 100000;
|
|
mc.y = 200000;
|
|
mc.z = 3;
|
|
mc.empire = 1;
|
|
feed(s, raw(mc));
|
|
CHECK(s.world().main_vid() == 7777, "main vid set");
|
|
const auto *me = s.world().get(7777);
|
|
CHECK(me && me->is_main && me->x == 100000.f && me->name == "Warrior", "main entity");
|
|
auto version = drain(s);
|
|
CHECK(version.size() == sizeof(CGClientVersion) + 1, "CG_CLIENT_VERSION + seq");
|
|
CGClientVersion cv{};
|
|
std::memcpy(&cv, version.data(), sizeof(cv));
|
|
CHECK(cv.header == HDR_CG_CLIENT_VERSION && std::strcmp(cv.filename, "metin2.bin") == 0 &&
|
|
std::strcmp(cv.timestamp, "1215955205") == 0, "client version fields");
|
|
CHECK(version[sizeof(CGClientVersion)] == SEQUENCE_TABLE[2], "version seq == table[2]");
|
|
|
|
GCCharacterAdd add{};
|
|
add.header = HDR_GC_CHARACTER_ADD;
|
|
add.vid = 8888;
|
|
add.race = 101;
|
|
add.type = 1; // NPC
|
|
add.x = 100500;
|
|
add.y = 200500;
|
|
add.moving_speed = 150;
|
|
feed(s, raw(add));
|
|
const auto *npc = s.world().get(8888);
|
|
CHECK(npc && npc->ch_type == 1 && npc->race == 101, "npc spawned");
|
|
CHECK(s.world().size() == 2, "2 entities");
|
|
|
|
GCShopSign sign{};
|
|
sign.header = HDR_GC_SHOP_SIGN;
|
|
sign.vid = 8888;
|
|
std::strcpy(sign.sign, "Potion shop");
|
|
feed(s, raw(sign));
|
|
CHECK(s.world().get(8888)->shop_sign == "Potion shop", "shop sign set");
|
|
std::memset(sign.sign, 0, sizeof(sign.sign));
|
|
feed(s, raw(sign));
|
|
CHECK(s.world().get(8888)->shop_sign.empty(), "shop sign cleared");
|
|
}
|
|
|
|
// --- auto CG_ENTERGAME after the loading delay ---
|
|
{
|
|
s.set_now(1499);
|
|
s.pump(); // no socket -> just the auto-entergame check + tick
|
|
CHECK(drain(s).empty(), "no ENTERGAME before delay");
|
|
s.set_now(1500);
|
|
s.pump();
|
|
auto out = drain(s);
|
|
CHECK(out.size() == sizeof(CGEnterGame) + 1, "CG_ENTERGAME + seq");
|
|
CHECK(out[0] == HDR_CG_ENTERGAME && out[1] == SEQUENCE_TABLE[3], "ENTERGAME + seq[3]");
|
|
}
|
|
|
|
// --- GC_PHASE(GAME) ---
|
|
{
|
|
Phase_ p{HDR_GC_PHASE, PHASE_GAME};
|
|
feed(s, raw(p));
|
|
CHECK(s.stage() == mtnet::INetSession::Stage::InGame, "PHASE_GAME -> InGame");
|
|
CHECK(s.in_game() && entered, "in_game() + on_entered_game");
|
|
}
|
|
|
|
// --- GC_MOVE(3): NPC starts moving, interpolation advances on tick ---
|
|
{
|
|
GCMove mv{};
|
|
mv.header = HDR_GC_MOVE;
|
|
mv.func = mtnet::FUNC_MOVE;
|
|
mv.rot = 64;
|
|
mv.vid = 8888;
|
|
mv.x = 110000;
|
|
mv.y = 200500;
|
|
mv.duration = 1000;
|
|
feed(s, raw(mv));
|
|
const auto *npc = s.world().get(8888);
|
|
CHECK(npc && npc->moving, "npc moving after GC_MOVE");
|
|
s.set_now(1500 + 500); // halfway through the 1000ms move (started at now=1500)
|
|
s.world().tick();
|
|
npc = s.world().get(8888);
|
|
CHECK(npc && npc->x > 100500.f && npc->x < 110000.f, "npc interpolated partway");
|
|
}
|
|
|
|
// --- GC_WARP(65): packed classic static packet must remain frameable ---
|
|
{
|
|
GCWarpClassic warp{};
|
|
warp.header = HDR_GC_WARP;
|
|
warp.x = 432100;
|
|
warp.y = 876500;
|
|
warp.addr = 0;
|
|
warp.port = 0;
|
|
feed(s, raw(warp));
|
|
auto warps = s.world().drain_warps();
|
|
CHECK(warps.size() == 1 && warps[0].x == 432100 && warps[0].y == 876500 &&
|
|
warps[0].same_server(), "classic GC_WARP parsed and surfaced");
|
|
}
|
|
|
|
// --- GC_CHARACTER_POINTS(16): stat block ---
|
|
{
|
|
GCPoints pts{};
|
|
pts.header = HDR_GC_CHARACTER_POINTS;
|
|
pts.points[mtnet::POINT_LEVEL] = 42;
|
|
pts.points[mtnet::POINT_MAX_HP] = 3500;
|
|
pts.points[mtnet::POINT_HP] = 3500;
|
|
feed(s, raw(pts));
|
|
CHECK(s.world().points().level() == 42, "points level");
|
|
CHECK(s.world().points().max_hp() == 3500, "points max_hp");
|
|
const auto *me = s.world().get(7777);
|
|
CHECK(me && me->max_hp == 3500 && me->level == 42, "vitals mirrored onto main entity");
|
|
}
|
|
|
|
// --- GC_CHARACTER_POINT_CHANGE(17): single stat delta (int32_t header quirk) ---
|
|
{
|
|
GCPointChange pc{};
|
|
pc.header = HDR_GC_CHARACTER_POINT_CHANGE;
|
|
pc.vid = 7777;
|
|
pc.type = mtnet::POINT_HP;
|
|
pc.value = 1200;
|
|
feed(s, raw(pc));
|
|
CHECK(s.world().points().hp() == 1200, "point change -> hp 1200");
|
|
CHECK(s.world().get(7777)->hp == 1200, "point change mirrored");
|
|
}
|
|
|
|
// --- in-game intents: CG_MOVE / ATTACK / CHAT / TARGET, each + a seq byte ---
|
|
{
|
|
// sequence index so far: [0]=CG_LOGIN, [1]=CG_CHARACTER_SELECT,
|
|
// [2]=CG_CLIENT_VERSION, [3]=CG_ENTERGAME.
|
|
CHECK(s.stream().sequence_index() == 4, "seq index at 4 before intents");
|
|
|
|
CHECK(s.send_move(mtnet::FUNC_MOVE, 0, 90.0f, 111111, 222222, 4242), "send_move");
|
|
auto out = drain(s);
|
|
CHECK(out.size() == sizeof(CGMove) + 1, "CG_MOVE + seq");
|
|
CGMove mv{};
|
|
std::memcpy(&mv, out.data(), sizeof(mv));
|
|
CHECK(mv.header == HDR_CG_MOVE && mv.func == mtnet::FUNC_MOVE, "move header/func");
|
|
CHECK(mv.rot == 18, "rot deg 90 -> wire 18 (deg/5)");
|
|
CHECK(mv.x == 111111 && mv.y == 222222 && mv.time == 4242, "move x/y/time");
|
|
CHECK(out[sizeof(CGMove)] == SEQUENCE_TABLE[4], "move seq == table[4]");
|
|
|
|
CHECK(s.send_attack(1, 8888), "send_attack");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGAttack) + 1 && out[0] == HDR_CG_ATTACK, "CG_ATTACK + seq");
|
|
CGAttack at{};
|
|
std::memcpy(&at, out.data(), sizeof(at));
|
|
CHECK(at.type == 1 && at.victim_vid == 8888, "attack fields");
|
|
CHECK(out[sizeof(CGAttack)] == SEQUENCE_TABLE[5], "attack seq == table[5]");
|
|
|
|
CHECK(s.send_target(8888), "send_target");
|
|
out = drain(s);
|
|
CGTarget tg{};
|
|
std::memcpy(&tg, out.data(), sizeof(tg));
|
|
CHECK(out.size() == sizeof(CGTarget) + 1 && tg.vid == 8888, "CG_TARGET + seq");
|
|
CHECK(out[sizeof(CGTarget)] == SEQUENCE_TABLE[6], "target seq == table[6]");
|
|
|
|
// dynamic CG_CHAT: [hdr][u16 len][type]"hello"\0 + seq
|
|
CHECK(s.send_chat(0, "hello"), "send_chat");
|
|
out = drain(s);
|
|
const size_t chat_len = sizeof(CGChatHead) + 5 + 1; // "hello" + NUL
|
|
CHECK(out.size() == chat_len + 1, "CG_CHAT wire size + seq");
|
|
CGChatHead ch{};
|
|
std::memcpy(&ch, out.data(), sizeof(ch));
|
|
CHECK(ch.header == HDR_CG_CHAT && ch.length == chat_len && ch.type == 0, "chat head");
|
|
CHECK(std::memcmp(out.data() + sizeof(CGChatHead), "hello\0", 6) == 0, "chat text + NUL");
|
|
CHECK(out[chat_len] == SEQUENCE_TABLE[7], "chat seq == table[7]");
|
|
|
|
CHECK(s.send_item_move(1, 5, 1, 9, 1), "send_item_move");
|
|
out = drain(s);
|
|
CGItemMove im{};
|
|
std::memcpy(&im, out.data(), sizeof(im));
|
|
CHECK(out.size() == sizeof(CGItemMove) + 1, "CG_ITEM_MOVE + seq");
|
|
CHECK(im.pos.window_type == 1 && im.pos.cell == 5 && im.change_pos.cell == 9 && im.num == 1,
|
|
"item_move fields");
|
|
}
|
|
|
|
// --- GC_ITEM_SET(21) / UPDATE(25) / DEL(20) — inventory ---
|
|
{
|
|
GCItemSet is{};
|
|
is.header = HDR_GC_ITEM_SET;
|
|
is.cell = {1, 4}; // inventory, cell 4
|
|
is.vnum = 27100; // a sword vnum
|
|
is.count = 1;
|
|
is.flags = 0x2;
|
|
is.anti_flags = 0x10;
|
|
is.sockets[0] = 28030;
|
|
is.attrs[0] = {5, 30};
|
|
is.attrs[1] = {1, 250};
|
|
feed(s, raw(is));
|
|
const mtnet::Item &it = s.world().item_slot(1, 4);
|
|
CHECK(it.vnum == 27100 && it.count == 1 && it.flags == 0x2 && it.anti_flags == 0x10,
|
|
"item_set landed");
|
|
CHECK(it.sockets[0] == 28030 && it.attrs[0].type == 5 && it.attrs[0].value == 30 &&
|
|
it.attrs[1].value == 250,
|
|
"item_set sockets/attrs decoded from 3-byte wire attr");
|
|
|
|
GCItemUpdate iu{};
|
|
iu.header = HDR_GC_ITEM_UPDATE;
|
|
iu.cell = {1, 4};
|
|
iu.count = 3;
|
|
feed(s, raw(iu));
|
|
CHECK(s.world().item_slot(1, 4).count == 3, "item_update count -> 3");
|
|
|
|
GCItemDel id{HDR_GC_ITEM_DEL, 4};
|
|
feed(s, raw(id));
|
|
CHECK(s.world().item_slot(1, 4).vnum == 0, "item_del cleared inventory cell 4");
|
|
}
|
|
|
|
// --- GC entity state: UPDATE(19) / CHANGE_SPEED(18) / POSITION(43) / MOTION(36) /
|
|
// STUN(13) / DEAD(14) / TARGET(63) ---
|
|
{
|
|
// re-add the NPC (it was despawned above? no — despawn is later). vid 8888 present.
|
|
GCCharacterUpdate cu{};
|
|
cu.header = HDR_GC_CHARACTER_UPDATE;
|
|
cu.vid = 8888;
|
|
cu.parts[0] = 1101;
|
|
cu.parts[1] = 2205;
|
|
cu.moving_speed = 180;
|
|
cu.attack_speed = 140;
|
|
cu.guild_id = 77;
|
|
cu.alignment = -50;
|
|
cu.pk_mode = 1;
|
|
cu.mount_vnum = 20101;
|
|
feed(s, raw(cu));
|
|
const auto *npc = s.world().get(8888);
|
|
CHECK(npc && npc->parts[0] == 1101 && npc->moving_speed == 180 && npc->guild == 77 &&
|
|
npc->alignment == -50 && npc->pk_mode == 1 && npc->mount_vnum == 20101,
|
|
"char_update applied");
|
|
|
|
GCChangeSpeed cs{HDR_GC_CHANGE_SPEED, 8888, 250};
|
|
feed(s, raw(cs));
|
|
CHECK(s.world().get(8888)->moving_speed == 250, "change_speed -> 250");
|
|
|
|
GCCharacterPosition cp{HDR_GC_CHARACTER_POSITION, 8888, 4};
|
|
feed(s, raw(cp));
|
|
CHECK(s.world().get(8888)->position == 4, "position -> 4");
|
|
|
|
GCMotion gm{HDR_GC_MOTION, 8888, 7777, 42};
|
|
feed(s, raw(gm));
|
|
auto ms = s.world().drain_motions();
|
|
CHECK(ms.size() == 1 && ms[0].vid == 8888 && ms[0].victim_vid == 7777 && ms[0].motion == 42,
|
|
"motion queued");
|
|
|
|
GCTarget gt{HDR_GC_TARGET, 8888, 65};
|
|
feed(s, raw(gt));
|
|
CHECK(s.world().target_vid() == 8888 && s.world().target_hp_pct() == 65, "target hp 65%");
|
|
|
|
GCStun st{HDR_GC_STUN, 8888};
|
|
feed(s, raw(st));
|
|
CHECK(s.world().get(8888)->stunned, "stun");
|
|
GCDead gd2{HDR_GC_DEAD, 8888};
|
|
feed(s, raw(gd2));
|
|
CHECK(s.world().get(8888)->dead && s.world().get(8888)->hp == 0, "dead + hp 0");
|
|
}
|
|
|
|
// --- GC_SKILL_LEVEL(76) / QUICKSLOT(28/29/30) / AFFECT(126/127) / DAMAGE(135) / WHISPER(34) ---
|
|
{
|
|
GCSkillLevel sk{};
|
|
sk.header = HDR_GC_SKILL_LEVEL;
|
|
sk.skills[1] = {1, 17}; // skill 1: master_type 1, level 17
|
|
sk.skills[50] = {0, 40}; // skill 50: normal, level 40
|
|
feed(s, raw(sk));
|
|
CHECK(s.world().skill_level(1) == 17 && s.world().skill_master(1) == 1, "skill 1");
|
|
CHECK(s.world().skill_level(50) == 40, "skill 50");
|
|
|
|
GCQuickSlotAdd qa{};
|
|
qa.header = HDR_GC_QUICKSLOT_ADD;
|
|
qa.pos = 3;
|
|
qa.slot = {2, 7}; // type=skill, ref=7
|
|
feed(s, raw(qa));
|
|
CHECK(s.world().quickslot(3).type == 2 && s.world().quickslot(3).position == 7, "quickslot 3");
|
|
GCQuickSlotSwap qs2{HDR_GC_QUICKSLOT_SWAP, 3, 8};
|
|
feed(s, raw(qs2));
|
|
CHECK(s.world().quickslot(8).type == 2 && s.world().quickslot(3).type == 0, "quickslot swap");
|
|
GCQuickSlotDel qd{HDR_GC_QUICKSLOT_DEL, 8};
|
|
feed(s, raw(qd));
|
|
CHECK(s.world().quickslot(8).type == 0, "quickslot del");
|
|
|
|
GCAffectAdd aa{};
|
|
aa.header = HDR_GC_AFFECT_ADD;
|
|
aa.elem = {90001, 16 /*ATT_GRADE*/, 50, 0, 300};
|
|
feed(s, raw(aa));
|
|
bool found = false;
|
|
for (const auto &a : s.world().affects()) {
|
|
if (a.type == 90001 && a.value == 50 && a.duration == 300) {
|
|
found = true;
|
|
}
|
|
}
|
|
CHECK(found, "affect_add landed");
|
|
GCAffectRemove ar{HDR_GC_AFFECT_REMOVE, 90001, 0};
|
|
feed(s, raw(ar));
|
|
CHECK(s.world().affects().empty(), "affect_remove cleared");
|
|
|
|
GCDamageInfo di{HDR_GC_DAMAGE_INFO, 7777, 0x1, 321};
|
|
feed(s, raw(di));
|
|
auto ds = s.world().drain_damage();
|
|
CHECK(ds.size() == 1 && ds[0].vid == 7777 && ds[0].amount == 321, "damage queued");
|
|
|
|
// GC_WHISPER: [hdr][u16 size][type][name_from[25]][text]
|
|
const char *msg = "hey there";
|
|
GCWhisperHead wh{};
|
|
wh.header = HDR_GC_WHISPER;
|
|
wh.type = 0;
|
|
std::strcpy(wh.name_from, "Gm");
|
|
wh.size = (uint16_t)(sizeof(GCWhisperHead) + std::strlen(msg));
|
|
std::vector<uint8_t> wp = raw(wh);
|
|
wp.insert(wp.end(), msg, msg + std::strlen(msg));
|
|
feed(s, wp);
|
|
auto cm = s.world().drain_chat();
|
|
CHECK(cm.size() == 1 && cm[0].type == mtnet::CHAT_TYPE_WHISPER && cm[0].from == "Gm" &&
|
|
cm[0].text == "hey there",
|
|
"whisper -> chat queue");
|
|
}
|
|
|
|
// --- GC party (77..92) ---
|
|
{
|
|
GCPartyParameter pp{HDR_GC_PARTY_PARAMETER, 1};
|
|
feed(s, raw(pp));
|
|
CHECK(s.world().party_distribute_mode() == 1, "party distribute mode 1");
|
|
|
|
GCPartyAdd pa{};
|
|
pa.header = HDR_GC_PARTY_ADD;
|
|
pa.pid = 5001;
|
|
std::strcpy(pa.name, "Mate");
|
|
feed(s, raw(pa));
|
|
const auto *pm = s.world().party_member(5001);
|
|
CHECK(pm && pm->name == "Mate", "party_add");
|
|
|
|
GCPartyUpdate pu{};
|
|
pu.header = HDR_GC_PARTY_UPDATE;
|
|
pu.pid = 5001;
|
|
pu.role = 1; // leader bit
|
|
pu.percent_hp = 80;
|
|
pu.affects[0] = 3;
|
|
feed(s, raw(pu));
|
|
pm = s.world().party_member(5001);
|
|
CHECK(pm && pm->leader() && pm->hp_pct == 80 && pm->affects[0] == 3, "party_update");
|
|
|
|
GCPartyLink pl{HDR_GC_PARTY_LINK, 5001, 8888};
|
|
feed(s, raw(pl));
|
|
CHECK(s.world().party_member(5001)->vid == 8888, "party_link -> vid");
|
|
GCPartyUnlink pul{HDR_GC_PARTY_UNLINK, 5001, 8888};
|
|
feed(s, raw(pul));
|
|
CHECK(s.world().party_member(5001)->vid == 0, "party_unlink");
|
|
|
|
GCPartyInvite pi{HDR_GC_PARTY_INVITE, 9999};
|
|
feed(s, raw(pi));
|
|
// (drained by pump_classic in the real client; here just verify no crash + member intact)
|
|
CHECK(s.world().party_member(5001) != nullptr, "party still intact after invite");
|
|
|
|
GCPartyRemove pr{HDR_GC_PARTY_REMOVE, 5001};
|
|
feed(s, raw(pr));
|
|
CHECK(s.world().party_member(5001) == nullptr, "party_remove");
|
|
}
|
|
|
|
// --- GC_SHOP(38, dynamic): START -> items, END -> closed, error sub ---
|
|
{
|
|
// [subheader=START][u32 vid][ShopItem43 x 40], only first 2 non-empty
|
|
std::vector<uint8_t> sp;
|
|
sp.push_back(SHOP_SUB_START);
|
|
uint32_t svid = 4242;
|
|
sp.insert(sp.end(), (uint8_t *)&svid, (uint8_t *)&svid + 4);
|
|
ShopItem43 items[SHOP_HOST_ITEM_MAX_NUM]{};
|
|
items[0].vnum = 27001;
|
|
items[0].price = 5000;
|
|
items[0].count = 1;
|
|
items[0].display_pos = 0;
|
|
items[2].vnum = 10;
|
|
items[2].price = 20;
|
|
items[2].count = 200;
|
|
items[2].display_pos = 2;
|
|
sp.insert(sp.end(), (uint8_t *)items, (uint8_t *)items + sizeof(items));
|
|
// wrap as GC_SHOP dynamic packet: [hdr 38][u16 size][body]
|
|
uint16_t sz = (uint16_t)(3 + sp.size());
|
|
std::vector<uint8_t> pkt;
|
|
pkt.push_back(HDR_GC_SHOP);
|
|
pkt.insert(pkt.end(), (uint8_t *)&sz, (uint8_t *)&sz + 2);
|
|
pkt.insert(pkt.end(), sp.begin(), sp.end());
|
|
feed(s, pkt);
|
|
CHECK(s.world().shop_open() && s.world().shop_vid() == 4242, "shop opened");
|
|
const auto &si = s.world().shop_items();
|
|
CHECK(si.size() == 2 && si[0].vnum == 27001 && si[0].price == 5000 && si[1].pos == 2,
|
|
"shop items (empties skipped, display_pos kept)");
|
|
|
|
// error sub-header
|
|
std::vector<uint8_t> ep = {HDR_GC_SHOP, 4, 0, SHOP_SUB_SOLDOUT};
|
|
feed(s, ep);
|
|
auto errs = s.world().drain_shop_errors();
|
|
CHECK(errs.size() == 1 && errs[0] == "SOLDOUT", "shop error SOLDOUT");
|
|
|
|
std::vector<uint8_t> ce = {HDR_GC_SHOP, 4, 0, SHOP_SUB_END};
|
|
feed(s, ce);
|
|
CHECK(!s.world().shop_open() && s.world().shop_items().empty(), "shop closed");
|
|
}
|
|
|
|
// --- GC_SCRIPT(45) / GC_QUEST_CONFIRM(46) / GC_QUEST_INFO(81) ---
|
|
{
|
|
// GC_SCRIPT dynamic: [hdr 45][u16 size][skin][u16 src_size][text]
|
|
const char *dlg = "Hello adventurer!";
|
|
std::vector<uint8_t> body;
|
|
body.push_back(2); // skin
|
|
body.push_back(0);
|
|
body.push_back(0); // src_size
|
|
body.insert(body.end(), dlg, dlg + std::strlen(dlg));
|
|
uint16_t sz = (uint16_t)(3 + body.size());
|
|
std::vector<uint8_t> pkt = {HDR_GC_SCRIPT};
|
|
pkt.insert(pkt.end(), (uint8_t *)&sz, (uint8_t *)&sz + 2);
|
|
pkt.insert(pkt.end(), body.begin(), body.end());
|
|
feed(s, pkt);
|
|
auto sc = s.world().drain_scripts();
|
|
CHECK(sc.size() == 1 && sc[0].skin == 2 && sc[0].text == "Hello adventurer!", "script cue");
|
|
|
|
GCQuestConfirm qc{};
|
|
qc.header = HDR_GC_QUEST_CONFIRM;
|
|
std::strcpy(qc.msg, "Accept quest?");
|
|
qc.timeout = 30;
|
|
qc.request_pid = 555;
|
|
feed(s, raw(qc));
|
|
auto cf = s.world().drain_confirms();
|
|
CHECK(cf.size() == 1 && cf[0].msg == "Accept quest?" && cf[0].timeout == 30 &&
|
|
cf[0].request_pid == 555,
|
|
"quest confirm cue");
|
|
|
|
// GC_QUEST_INFO dynamic: [hdr 81][u16 size][u16 index][u8 flag][TITLE\0][COUNTER_NAME\0][i32]
|
|
std::vector<uint8_t> qb;
|
|
uint16_t qidx = 3;
|
|
qb.insert(qb.end(), (uint8_t *)&qidx, (uint8_t *)&qidx + 2);
|
|
uint8_t qflag = QUEST_SEND_TITLE | QUEST_SEND_COUNTER_NAME | QUEST_SEND_COUNTER_VALUE;
|
|
qb.push_back(qflag);
|
|
const char *qt = "Kill 10 wolves";
|
|
qb.insert(qb.end(), qt, qt + std::strlen(qt) + 1);
|
|
const char *cn = "Wolves";
|
|
qb.insert(qb.end(), cn, cn + std::strlen(cn) + 1);
|
|
int32_t cv = 4;
|
|
qb.insert(qb.end(), (uint8_t *)&cv, (uint8_t *)&cv + 4);
|
|
uint16_t qsz = (uint16_t)(3 + qb.size());
|
|
std::vector<uint8_t> qpkt = {HDR_GC_QUEST_INFO};
|
|
qpkt.insert(qpkt.end(), (uint8_t *)&qsz, (uint8_t *)&qsz + 2);
|
|
qpkt.insert(qpkt.end(), qb.begin(), qb.end());
|
|
feed(s, qpkt);
|
|
const mtnet::QuestInfo *q = s.world().quest(3);
|
|
CHECK(q && q->title == "Kill 10 wolves" && q->counter_name == "Wolves" &&
|
|
q->counter_value == 4,
|
|
"quest info parsed (flag-driven)");
|
|
}
|
|
|
|
// --- GC_DUEL_START dynamic: [hdr 40][u16 whole_size][opponent VIDs] ---
|
|
{
|
|
uint32_t opponent_vids[] = {41001, 41002};
|
|
uint16_t duel_size = (uint16_t)(sizeof(GCDuelStart) + sizeof(opponent_vids));
|
|
std::vector<uint8_t> duel_pkt = {HDR_GC_DUEL_START};
|
|
duel_pkt.insert(duel_pkt.end(), (uint8_t *)&duel_size, (uint8_t *)&duel_size + 2);
|
|
duel_pkt.insert(duel_pkt.end(), (uint8_t *)opponent_vids,
|
|
(uint8_t *)opponent_vids + sizeof(opponent_vids));
|
|
feed(s, duel_pkt);
|
|
const auto &opponents = s.world().duel_opponents();
|
|
CHECK(opponents.size() == 2 && opponents[0] == 41001 && opponents[1] == 41002 &&
|
|
!s.world().duel_cannot_attack(),
|
|
"duel start opponent list");
|
|
CHECK(s.world().take_duel_started(), "duel start event");
|
|
|
|
uint16_t empty_duel_size = sizeof(GCDuelStart);
|
|
std::vector<uint8_t> empty_duel = {HDR_GC_DUEL_START};
|
|
empty_duel.insert(empty_duel.end(), (uint8_t *)&empty_duel_size,
|
|
(uint8_t *)&empty_duel_size + 2);
|
|
feed(s, empty_duel);
|
|
CHECK(s.world().duel_opponents().empty() && s.world().duel_cannot_attack(),
|
|
"empty duel start cannot-attack state");
|
|
}
|
|
|
|
// --- P8 messenger (74, dynamic): complete list + login/logout deltas ---
|
|
{
|
|
std::vector<uint8_t> body = {MESSENGER_GC_LIST, 1, 5};
|
|
body.insert(body.end(), {'A', 'l', 'i', 'c', 'e'});
|
|
body.push_back(0);
|
|
body.push_back(3);
|
|
body.insert(body.end(), {'B', 'o', 'b'});
|
|
GCMessengerHead h{HDR_GC_MESSENGER,
|
|
static_cast<uint16_t>(sizeof(GCMessengerHead) + body.size() - 1), body[0]};
|
|
std::vector<uint8_t> pkt = raw(h);
|
|
pkt.insert(pkt.end(), body.begin() + 1, body.end());
|
|
feed(s, pkt);
|
|
auto fs = s.world().friends();
|
|
CHECK(fs.size() == 2, "messenger list friend count");
|
|
bool alice = false, bob = false;
|
|
for (const auto &f : fs) {
|
|
alice = alice || (f.name == "Alice" && f.online);
|
|
bob = bob || (f.name == "Bob" && !f.online);
|
|
}
|
|
CHECK(alice && bob, "messenger list online state");
|
|
|
|
std::vector<uint8_t> login_body = {MESSENGER_GC_LOGIN, 3, 'B', 'o', 'b'};
|
|
GCMessengerHead login_h{HDR_GC_MESSENGER,
|
|
static_cast<uint16_t>(sizeof(GCMessengerHead) + login_body.size() - 1),
|
|
login_body[0]};
|
|
std::vector<uint8_t> login_pkt = raw(login_h);
|
|
login_pkt.insert(login_pkt.end(), login_body.begin() + 1, login_body.end());
|
|
feed(s, login_pkt);
|
|
fs = s.world().friends();
|
|
bool bob_online = false;
|
|
for (const auto &f : fs) {
|
|
bob_online = bob_online || (f.name == "Bob" && f.online);
|
|
}
|
|
CHECK(fs.size() == 2 && bob_online, "messenger login update");
|
|
|
|
std::vector<uint8_t> logout_body = {MESSENGER_GC_LOGOUT, 5, 'A', 'l', 'i', 'c', 'e'};
|
|
GCMessengerHead logout_h{HDR_GC_MESSENGER,
|
|
static_cast<uint16_t>(sizeof(GCMessengerHead) + logout_body.size() - 1),
|
|
logout_body[0]};
|
|
std::vector<uint8_t> logout_pkt = raw(logout_h);
|
|
logout_pkt.insert(logout_pkt.end(), logout_body.begin() + 1, logout_body.end());
|
|
feed(s, logout_pkt);
|
|
fs = s.world().friends();
|
|
bool alice_offline = false;
|
|
for (const auto &f : fs) {
|
|
alice_offline = alice_offline || (f.name == "Alice" && !f.online);
|
|
}
|
|
CHECK(fs.size() == 2 && alice_offline, "messenger logout update");
|
|
|
|
std::vector<uint8_t> mobile_body = {MESSENGER_GC_MOBILE, 1, 5,
|
|
'A', 'l', 'i', 'c', 'e'};
|
|
GCMessengerHead mobile_h{HDR_GC_MESSENGER,
|
|
static_cast<uint16_t>(sizeof(GCMessengerHead) + mobile_body.size() - 1),
|
|
mobile_body[0]};
|
|
std::vector<uint8_t> mobile_pkt = raw(mobile_h);
|
|
mobile_pkt.insert(mobile_pkt.end(), mobile_body.begin() + 1, mobile_body.end());
|
|
feed(s, mobile_pkt);
|
|
bool alice_mobile = false;
|
|
for (const auto &f : s.world().friends()) {
|
|
alice_mobile = alice_mobile || (f.name == "Alice" && f.mobile);
|
|
}
|
|
CHECK(alice_mobile, "messenger mobile state");
|
|
}
|
|
|
|
// --- P8 exchange (42, static) -------------------------------------------
|
|
{
|
|
GCExchange ex{};
|
|
ex.header = HDR_GC_EXCHANGE;
|
|
ex.subheader = EXCHANGE_GC_START;
|
|
ex.arg1 = 31337;
|
|
feed(s, raw(ex));
|
|
CHECK(s.world().exchange().active && s.world().exchange().partner_vid == 31337,
|
|
"exchange start");
|
|
|
|
ex = GCExchange{};
|
|
ex.header = HDR_GC_EXCHANGE;
|
|
ex.subheader = EXCHANGE_GC_ITEM_ADD;
|
|
ex.is_me = 1;
|
|
ex.arg1 = 27001;
|
|
ex.arg2 = {0, 4};
|
|
ex.arg3 = 7;
|
|
feed(s, raw(ex));
|
|
CHECK(s.world().exchange().self_items[4].vnum == 27001 &&
|
|
s.world().exchange().self_items[4].count == 7, "exchange item add");
|
|
|
|
ex.subheader = EXCHANGE_GC_GOLD_ADD;
|
|
ex.arg1 = 123456;
|
|
feed(s, raw(ex));
|
|
CHECK(s.world().exchange().self_gold == 123456, "exchange gold");
|
|
ex.subheader = EXCHANGE_GC_ACCEPT;
|
|
ex.arg1 = 1;
|
|
feed(s, raw(ex));
|
|
CHECK(s.world().exchange().self_accept, "exchange accept");
|
|
ex.subheader = EXCHANGE_GC_END;
|
|
feed(s, raw(ex));
|
|
CHECK(!s.world().exchange().active, "exchange end");
|
|
}
|
|
|
|
// --- Guild dynamic packets: members, presence, comments, invite, score --
|
|
{
|
|
GuildMember38 member{};
|
|
member.pid = 7001;
|
|
member.grade = 1;
|
|
member.is_general = 1;
|
|
member.job = 2;
|
|
member.level = 55;
|
|
member.offer = 1234;
|
|
member.name_flag = 1;
|
|
std::strcpy(member.name, "Guildmate");
|
|
std::vector<uint8_t> body = {mtnet::GUILD_GC_LIST};
|
|
auto member_bytes = raw(member);
|
|
body.insert(body.end(), member_bytes.begin(), member_bytes.end());
|
|
GCMessengerHead gh{HDR_GC_GUILD,
|
|
static_cast<uint16_t>(sizeof(GCMessengerHead) + body.size() - 1), body[0]};
|
|
std::vector<uint8_t> guild_pkt = raw(gh);
|
|
guild_pkt.insert(guild_pkt.end(), body.begin() + 1, body.end());
|
|
feed(s, guild_pkt);
|
|
auto members = s.world().guild_members();
|
|
CHECK(members.size() == 1 && members[0].pid == 7001 && members[0].name == "Guildmate" &&
|
|
members[0].is_general, "guild member list");
|
|
|
|
std::vector<uint8_t> login_body = {mtnet::GUILD_GC_LOGIN, 0, 0, 0, 0};
|
|
std::memcpy(login_body.data() + 1, &member.pid, sizeof(member.pid));
|
|
gh.size = static_cast<uint16_t>(sizeof(GCMessengerHead) + login_body.size() - 1);
|
|
gh.subheader = login_body[0];
|
|
guild_pkt = raw(gh);
|
|
guild_pkt.insert(guild_pkt.end(), login_body.begin() + 1, login_body.end());
|
|
feed(s, guild_pkt);
|
|
members = s.world().guild_members();
|
|
CHECK(members.size() == 1 && members[0].online, "guild member login");
|
|
|
|
GuildComment80 comment{};
|
|
comment.id = 17;
|
|
std::strcpy(comment.name, "Guildmate");
|
|
std::strcpy(comment.content, "Welcome to the guild");
|
|
std::vector<uint8_t> comments_body = {mtnet::GUILD_GC_COMMENTS, 1};
|
|
auto comment_bytes = raw(comment);
|
|
comments_body.insert(comments_body.end(), comment_bytes.begin(), comment_bytes.end());
|
|
gh.size = static_cast<uint16_t>(sizeof(GCMessengerHead) + comments_body.size() - 1);
|
|
gh.subheader = comments_body[0];
|
|
guild_pkt = raw(gh);
|
|
guild_pkt.insert(guild_pkt.end(), comments_body.begin() + 1, comments_body.end());
|
|
feed(s, guild_pkt);
|
|
const auto &comments = s.world().guild_comments();
|
|
CHECK(comments.size() == 1 && comments[0].id == 17 &&
|
|
comments[0].content == "Welcome to the guild", "guild comments");
|
|
|
|
GuildInvite17 invite{9001, "Wolves"};
|
|
std::vector<uint8_t> invite_body = {mtnet::GUILD_GC_GUILD_INVITE};
|
|
auto invite_bytes = raw(invite);
|
|
invite_body.insert(invite_body.end(), invite_bytes.begin(), invite_bytes.end());
|
|
gh.size = static_cast<uint16_t>(sizeof(GCMessengerHead) + invite_body.size() - 1);
|
|
gh.subheader = invite_body[0];
|
|
guild_pkt = raw(gh);
|
|
guild_pkt.insert(guild_pkt.end(), invite_body.begin() + 1, invite_body.end());
|
|
feed(s, guild_pkt);
|
|
auto invites = s.world().drain_guild_invites();
|
|
CHECK(invites.size() == 1 && invites[0].guild_id == 9001 &&
|
|
invites[0].guild_name == "Wolves", "guild invite");
|
|
|
|
GuildWarPoint12 score{77, 88, 12};
|
|
std::vector<uint8_t> score_body = {mtnet::GUILD_GC_WAR_POINT};
|
|
auto score_bytes = raw(score);
|
|
score_body.insert(score_body.end(), score_bytes.begin(), score_bytes.end());
|
|
gh.size = static_cast<uint16_t>(sizeof(GCMessengerHead) + score_body.size() - 1);
|
|
gh.subheader = score_body[0];
|
|
guild_pkt = raw(gh);
|
|
guild_pkt.insert(guild_pkt.end(), score_body.begin() + 1, score_body.end());
|
|
feed(s, guild_pkt);
|
|
auto scores = s.world().drain_guild_war_scores();
|
|
CHECK(scores.size() == 1 && scores[0].gain_guild_id == 77 && scores[0].point == 12,
|
|
"guild war score");
|
|
}
|
|
|
|
// --- P8 safebox / mall fixed packets ------------------------------------
|
|
{
|
|
GCSafeboxSize sb_size{HDR_GC_SAFEBOX_SIZE, 2};
|
|
feed(s, raw(sb_size));
|
|
GCSafeboxMoneyChange sb_money{HDR_GC_SAFEBOX_MONEY_CHANGE, 987654};
|
|
feed(s, raw(sb_money));
|
|
GCItemSet sb_item{};
|
|
sb_item.header = HDR_GC_SAFEBOX_SET;
|
|
sb_item.cell = {3, 8};
|
|
sb_item.vnum = 50001;
|
|
sb_item.count = 9;
|
|
sb_item.sockets[0] = 60001;
|
|
sb_item.attrs[0] = {5, 30};
|
|
feed(s, raw(sb_item));
|
|
CHECK(s.world().safebox_open() && s.world().safebox_size() == 2 &&
|
|
s.world().safebox_gold() == 987654, "safebox open/money");
|
|
CHECK(s.world().safebox_slot(8).vnum == 50001 && s.world().safebox_slot(8).count == 9 &&
|
|
s.world().safebox_slot(8).sockets[0] == 60001 &&
|
|
s.world().safebox_slot(8).attrs[0].value == 30, "safebox item set");
|
|
GCItemDel sb_del{HDR_GC_SAFEBOX_DEL, 8};
|
|
feed(s, raw(sb_del));
|
|
CHECK(s.world().safebox_slot(8).empty(), "safebox item del");
|
|
|
|
GCMallOpen mall_open{HDR_GC_MALL_OPEN, 1};
|
|
feed(s, raw(mall_open));
|
|
GCItemSet mall_item{};
|
|
mall_item.header = HDR_GC_MALL_SET;
|
|
mall_item.cell = {4, 3};
|
|
mall_item.vnum = 70001;
|
|
mall_item.count = 2;
|
|
feed(s, raw(mall_item));
|
|
CHECK(s.world().mall_open() && s.world().mall_size() == 1 &&
|
|
s.world().mall_slot(3).vnum == 70001, "mall open/item set");
|
|
GCItemDel mall_del{HDR_GC_MALL_DEL, 3};
|
|
feed(s, raw(mall_del));
|
|
CHECK(s.world().mall_slot(3).empty(), "mall item del");
|
|
}
|
|
|
|
// --- P8 outbound intents: fixed packets carry one trailing sequence byte ---
|
|
{
|
|
uint32_t seq = s.stream().sequence_index();
|
|
CHECK(s.send_friend_add("Alice"), "send_friend_add");
|
|
auto out = drain(s);
|
|
CHECK(out.size() == sizeof(CGMessenger) + CHARACTER_NAME_MAX_LEN + 1 &&
|
|
out[0] == HDR_CG_MESSENGER && out[1] == MESSENGER_CG_ADD_BY_NAME &&
|
|
std::memcmp(out.data() + sizeof(CGMessenger), "Alice", 5) == 0 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "friend add wire + sequence");
|
|
++seq;
|
|
|
|
CHECK(s.send_friend_remove("Alice"), "send_friend_remove");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGMessenger) + CHARACTER_NAME_MAX_LEN + 1 &&
|
|
out[1] == MESSENGER_CG_REMOVE && out.back() == SEQUENCE_TABLE[seq],
|
|
"friend remove wire + sequence");
|
|
++seq;
|
|
|
|
CHECK(s.send_exchange_start(31337), "send_exchange_start");
|
|
out = drain(s);
|
|
CGExchange cg_ex{};
|
|
std::memcpy(&cg_ex, out.data(), sizeof(cg_ex));
|
|
CHECK(out.size() == sizeof(CGExchange) + 1 && cg_ex.subheader == EXCHANGE_CG_START &&
|
|
cg_ex.arg1 == 31337 && out.back() == SEQUENCE_TABLE[seq],
|
|
"exchange start wire + sequence");
|
|
++seq;
|
|
|
|
CHECK(s.send_exchange_item_add(1, 12, 4), "send_exchange_item_add");
|
|
out = drain(s);
|
|
std::memcpy(&cg_ex, out.data(), sizeof(cg_ex));
|
|
CHECK(cg_ex.subheader == EXCHANGE_CG_ITEM_ADD && cg_ex.arg2 == 4 &&
|
|
cg_ex.pos.window_type == 1 && cg_ex.pos.cell == 12 && out.back() == SEQUENCE_TABLE[seq],
|
|
"exchange item wire");
|
|
++seq;
|
|
|
|
CHECK(s.send_exchange_gold(900), "send_exchange_gold");
|
|
out = drain(s);
|
|
std::memcpy(&cg_ex, out.data(), sizeof(cg_ex));
|
|
CHECK(cg_ex.subheader == EXCHANGE_CG_GOLD_ADD && cg_ex.arg1 == 900 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "exchange gold wire");
|
|
++seq;
|
|
|
|
CHECK(s.send_exchange_accept(), "send_exchange_accept");
|
|
out = drain(s);
|
|
std::memcpy(&cg_ex, out.data(), sizeof(cg_ex));
|
|
CHECK(cg_ex.subheader == EXCHANGE_CG_ACCEPT && out.back() == SEQUENCE_TABLE[seq],
|
|
"exchange accept wire");
|
|
++seq;
|
|
|
|
CHECK(s.send_exchange_cancel(), "send_exchange_cancel");
|
|
out = drain(s);
|
|
std::memcpy(&cg_ex, out.data(), sizeof(cg_ex));
|
|
CHECK(cg_ex.subheader == EXCHANGE_CG_CANCEL && out.back() == SEQUENCE_TABLE[seq],
|
|
"exchange cancel wire");
|
|
++seq;
|
|
|
|
CHECK(s.send_safebox_checkin(8, 1, 12), "send_safebox_checkin");
|
|
out = drain(s);
|
|
CGSafeboxCheckin checkin{};
|
|
std::memcpy(&checkin, out.data(), sizeof(checkin));
|
|
CHECK(checkin.header == HDR_CG_SAFEBOX_CHECKIN && checkin.safe_pos == 8 &&
|
|
checkin.item_pos.window_type == 1 && checkin.item_pos.cell == 12 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "safebox checkin wire");
|
|
++seq;
|
|
|
|
CHECK(s.send_safebox_checkout(8, 1, 12), "send_safebox_checkout");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGSafeboxCheckout) + 1 &&
|
|
out[0] == HDR_CG_SAFEBOX_CHECKOUT && out.back() == SEQUENCE_TABLE[seq],
|
|
"safebox checkout wire");
|
|
++seq;
|
|
|
|
CHECK(s.send_safebox_move(8, 9, 2), "send_safebox_move");
|
|
out = drain(s);
|
|
CGItemMove move{};
|
|
std::memcpy(&move, out.data(), sizeof(move));
|
|
CHECK(move.header == HDR_CG_SAFEBOX_ITEM_MOVE && move.pos.window_type == 1 &&
|
|
move.pos.cell == 8 && move.change_pos.cell == 9 && move.num == 2 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "safebox move wire");
|
|
++seq;
|
|
|
|
CHECK(s.send_mall_checkout(3, 1, 22), "send_mall_checkout");
|
|
out = drain(s);
|
|
CGMallCheckout mall{};
|
|
std::memcpy(&mall, out.data(), sizeof(mall));
|
|
CHECK(mall.header == HDR_CG_MALL_CHECKOUT && mall.mall_pos == 3 &&
|
|
mall.item_pos.window_type == 1 && mall.item_pos.cell == 22 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "mall checkout wire");
|
|
CHECK(s.stream().sequence_index() == seq + 1, "P8 sequence index advanced");
|
|
}
|
|
|
|
// --- 40250 command-backed cube intents -------------------------------
|
|
{
|
|
uint32_t seq = s.stream().sequence_index();
|
|
CHECK(s.send_cube_open(), "cube open");
|
|
auto out = drain(s);
|
|
CHECK(out.size() == sizeof(CGChatHead) + 10 + 1 + 1 &&
|
|
std::memcmp(out.data() + sizeof(CGChatHead), "/cube open", 10) == 0 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "cube open command wire");
|
|
++seq;
|
|
CHECK(s.send_cube_add_item(2, 17), "cube add");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGChatHead) + 14 + 1 + 1 &&
|
|
std::memcmp(out.data() + sizeof(CGChatHead), "/cube add 2 17", 14) == 0 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "cube add command wire");
|
|
++seq;
|
|
CHECK(s.send_cube_delete_item(2), "cube delete");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGChatHead) + 14 + 1 + 1 &&
|
|
std::memcmp(out.data() + sizeof(CGChatHead), "/cube delete 2", 14) == 0 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "cube delete command wire");
|
|
++seq;
|
|
CHECK(s.send_cube_list(), "cube list");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGChatHead) + 10 + 1 + 1 &&
|
|
std::memcmp(out.data() + sizeof(CGChatHead), "/cube list", 10) == 0 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "cube list command wire");
|
|
++seq;
|
|
CHECK(s.send_cube_close(), "cube close");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGChatHead) + 11 + 1 + 1 &&
|
|
std::memcmp(out.data() + sizeof(CGChatHead), "/cube close", 11) == 0 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "cube close command wire");
|
|
CHECK(!s.send_cube_add_item(24, 17), "cube index bounds");
|
|
}
|
|
|
|
// --- Guild comment/invite answer intents -------------------------------
|
|
{
|
|
uint32_t seq = s.stream().sequence_index();
|
|
CHECK(s.send_guild_comment("hello"), "send guild comment");
|
|
auto out = drain(s);
|
|
CHECK(out.size() == sizeof(CGGuild) + 1 + 5 + 1 + 1 && out[0] == HDR_CG_GUILD &&
|
|
out[1] == GUILD_CG_POST_COMMENT && out[2] == 6 &&
|
|
std::memcmp(out.data() + sizeof(CGGuild) + 1, "hello", 5) == 0 &&
|
|
out[sizeof(CGGuild) + 1 + 5] == 0 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "guild comment wire");
|
|
++seq;
|
|
CHECK(s.send_guild_grade_name(2, "Officer"), "send guild grade name");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGGuild) + 1 + 9 + 1 &&
|
|
out[1] == GUILD_CG_CHANGE_GRADE_NAME && out[2] == 2 &&
|
|
std::memcmp(out.data() + sizeof(CGGuild) + 1, "Officer", 7) == 0 &&
|
|
out[sizeof(CGGuild) + 1 + 7] == 0 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "guild grade name wire");
|
|
++seq;
|
|
CHECK(s.send_guild_grade_authority(2, 7), "send guild grade authority");
|
|
out = drain(s);
|
|
CHECK(out.size() == sizeof(CGGuild) + 2 + 1 &&
|
|
out[1] == GUILD_CG_CHANGE_GRADE_AUTHORITY && out[2] == 2 && out[3] == 7 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "guild grade authority wire");
|
|
++seq;
|
|
CHECK(s.send_guild_member_grade(1234, 3), "send guild member grade");
|
|
out = drain(s);
|
|
uint32_t pid = 0;
|
|
std::memcpy(&pid, out.data() + sizeof(CGGuild), sizeof(pid));
|
|
CHECK(out.size() == sizeof(CGGuild) + 4 + 1 + 1 &&
|
|
out[1] == GUILD_CG_CHANGE_MEMBER_GRADE && pid == 1234 &&
|
|
out[sizeof(CGGuild) + 4] == 3 && out.back() == SEQUENCE_TABLE[seq],
|
|
"guild member grade wire");
|
|
++seq;
|
|
CHECK(s.send_guild_member_general(1234, true), "send guild member general");
|
|
out = drain(s);
|
|
std::memcpy(&pid, out.data() + sizeof(CGGuild), sizeof(pid));
|
|
CHECK(out.size() == sizeof(CGGuild) + 4 + 1 + 1 &&
|
|
out[1] == GUILD_CG_CHANGE_MEMBER_GENERAL && pid == 1234 &&
|
|
out[sizeof(CGGuild) + 4] == 1 && out.back() == SEQUENCE_TABLE[seq],
|
|
"guild member general wire");
|
|
++seq;
|
|
CHECK(s.send_guild_invite_answer(9001, true), "send guild invite answer");
|
|
out = drain(s);
|
|
uint32_t gid = 0;
|
|
std::memcpy(&gid, out.data() + sizeof(CGGuild), sizeof(gid));
|
|
CHECK(out.size() == sizeof(CGGuild) + 4 + 1 + 1 && out[1] == GUILD_CG_GUILD_INVITE_ANSWER &&
|
|
gid == 9001 && out[sizeof(CGGuild) + 4] == 1 &&
|
|
out.back() == SEQUENCE_TABLE[seq], "guild invite answer wire");
|
|
}
|
|
|
|
// --- GC_CHAR_ADDITIONAL_INFO(136): fills a spawned entity's name/parts/level ---
|
|
{
|
|
GCCharAddInfo ci{};
|
|
ci.header = HDR_GC_CHAR_ADDITIONAL_INFO;
|
|
ci.vid = 8888;
|
|
std::strcpy(ci.name, "Goblin");
|
|
ci.parts[0] = 3;
|
|
ci.empire = 1;
|
|
ci.guild_id = 0;
|
|
ci.level = 12;
|
|
feed(s, raw(ci));
|
|
const auto *e = s.world().get(8888);
|
|
CHECK(e && e->name == "Goblin" && e->level == 12 && e->parts[0] == 3, "char_add_info");
|
|
}
|
|
|
|
// --- GC_SYNC_POSITION(5, dynamic): snap entities ---
|
|
{
|
|
struct {
|
|
uint8_t hdr;
|
|
uint16_t size;
|
|
} __attribute__((packed)) sph{HDR_GC_SYNC_POSITION, 0};
|
|
SyncPosElement el{8888, 999000, 888000};
|
|
sph.size = (uint16_t)(sizeof(sph) + sizeof(el));
|
|
std::vector<uint8_t> sp(sizeof(sph));
|
|
std::memcpy(sp.data(), &sph, sizeof(sph));
|
|
auto eb = raw(el);
|
|
sp.insert(sp.end(), eb.begin(), eb.end());
|
|
feed(s, sp);
|
|
const auto *e = s.world().get(8888);
|
|
CHECK(e && e->x == 999000.f && e->y == 888000.f && !e->moving, "sync_position snapped");
|
|
}
|
|
|
|
// --- GC_ITEM_GROUND_ADD(26) / DEL(27) ---
|
|
{
|
|
GCItemGroundAdd ga{};
|
|
ga.header = HDR_GC_ITEM_GROUND_ADD;
|
|
ga.vid = 55555;
|
|
ga.vnum = 1;
|
|
ga.x = 100200;
|
|
ga.y = 200300;
|
|
ga.z = 4;
|
|
feed(s, raw(ga));
|
|
CHECK(s.world().ground(55555) != nullptr, "ground item added");
|
|
GCItemGroundDel gd{HDR_GC_ITEM_GROUND_DEL, 55555};
|
|
feed(s, raw(gd));
|
|
CHECK(s.world().ground(55555) == nullptr, "ground item removed");
|
|
}
|
|
|
|
// --- GC_CHARACTER_DEL(2) ---
|
|
{
|
|
GCCharacterDel del{HDR_GC_CHARACTER_DEL, 8888};
|
|
feed(s, raw(del));
|
|
CHECK(s.world().get(8888) == nullptr, "npc despawned");
|
|
CHECK(s.world().size() == 1, "1 entity left");
|
|
}
|
|
|
|
// --- intents are rejected outside PHASE_GAME ---
|
|
{
|
|
ClassicSession s2;
|
|
CHECK(!s2.send_move(1, 0, 0, 0, 0), "send_move rejected before InGame");
|
|
CHECK(!s2.send_chat(0, "hi"), "send_chat rejected before InGame");
|
|
}
|
|
|
|
CHECK(s.last_error().empty(), "no error over the whole flow");
|
|
|
|
if (g_fail) {
|
|
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
|
|
return 1;
|
|
}
|
|
std::puts("net_classic_session_test OK");
|
|
return 0;
|
|
}
|