Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
phases, EntityStore world model, ~all GC/CG headers. char create/delete,
private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
token), system-option + game-option + ESC system menu, private-shop 39-grid,
party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.
Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.
Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).
ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
468 lines
18 KiB
C++
468 lines
18 KiB
C++
// net_e2e — drive the full client login flow against a REAL server:
|
|
// auth handshake -> CG_LOGIN3 -> GC_AUTH_SUCCESS
|
|
// -> game server: CG_LOGIN2 -> char list -> select_character -> PHASE_GAME
|
|
// -> pump a few seconds, dump entities / points / inventory / party.
|
|
//
|
|
// net_e2e [auth_host] [auth_port] [game_host] [game_port] [id] [pw] [char_index] [seconds]
|
|
// defaults: 192.168.21.203 11000 192.168.21.203 11011 admin 123456789 0 8
|
|
// MT_E2E_SWEEP=1 enables the one-session safe protocol matrix; add
|
|
// MT_E2E_UNSAFE=1 only when warp/dungeon probes are explicitly authorized.
|
|
//
|
|
// Mirrors M2Client::pump_auth/pump_game orchestration without the Godot layer.
|
|
#include "../src/net/auth_client.h"
|
|
#include "../src/net/game_client.h"
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
using clock_t_ = std::chrono::steady_clock;
|
|
|
|
static double elapsed(clock_t_::time_point start) {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(clock_t_::now() - start).count() /
|
|
1000.0;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
std::string auth_host = argc > 1 ? argv[1] : "192.168.21.203";
|
|
uint16_t auth_port = argc > 2 ? (uint16_t)std::stoi(argv[2]) : 11000;
|
|
std::string game_host = argc > 3 ? argv[3] : "192.168.21.203";
|
|
uint16_t game_port = argc > 4 ? (uint16_t)std::stoi(argv[4]) : 11011;
|
|
std::string id = argc > 5 ? argv[5] : "admin";
|
|
std::string pw = argc > 6 ? argv[6] : "123456789";
|
|
int char_index = argc > 7 ? std::atoi(argv[7]) : 0;
|
|
double run_secs = argc > 8 ? std::atof(argv[8]) : 8.0;
|
|
|
|
std::printf("net_e2e -> auth %s:%u game %s:%u id=%s char=%d\n", auth_host.c_str(), auth_port,
|
|
game_host.c_str(), game_port, id.c_str(), char_index);
|
|
|
|
bool trace = std::getenv("MT_NET_TRACE") != nullptr;
|
|
|
|
// ---- phase 1: auth server ------------------------------------------------
|
|
mtnet::AuthClient auth(id, pw);
|
|
auth.set_wire_trace(trace);
|
|
if (!auth.connect(auth_host, auth_port)) {
|
|
std::printf("FAIL: auth connect(): %s\n", auth.last_error().c_str());
|
|
return 2;
|
|
}
|
|
auto start = clock_t_::now();
|
|
while (!auth.done() && elapsed(start) < 15.0) {
|
|
auth.process();
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
|
}
|
|
if (!auth.done() || !auth.success()) {
|
|
std::printf("FAIL: auth (%s)\n", auth.fail_reason().empty() ? auth.last_error().c_str()
|
|
: auth.fail_reason().c_str());
|
|
return 3;
|
|
}
|
|
uint32_t login_key = auth.login_key();
|
|
std::printf("[auth] OK login_key=0x%08X\n", login_key);
|
|
auth.disconnect();
|
|
|
|
// ---- phase 2: game server ---------------------------------------------
|
|
mtnet::GameClient game(id, login_key);
|
|
game.set_wire_trace(trace);
|
|
game.set_auto_enter_game(false); // this tool sends it on its own schedule
|
|
if (std::getenv("MT_NET_DUMP")) {
|
|
for (uint16_t h : {0x0205, 0x0206, 0x0207, 0x0209, 0x020A, 0x0214, 0x0215,
|
|
// round 2
|
|
0x021B, 0x0519, 0x051A, 0x051B, 0x0730, 0x0A20, 0x0514, 0x0A30,
|
|
0x0A31, 0x0A50, 0x0912, 0x0603, 0x0304, 0x0307, 0x0217, 0x0216,
|
|
0x0410, 0x0413, 0x0A11, 0x0A12, 0x0A13,
|
|
// post-2026-08-30: create/delete, dragon soul, mall
|
|
0x020C, 0x020D, 0x020E, 0x020F, 0x051F, 0x0841, 0x0842, 0x0843, 0x0109}) {
|
|
game.dump_header(h);
|
|
}
|
|
}
|
|
const bool e2e_charcreate = std::getenv("MT_E2E_CHARCREATE") != nullptr;
|
|
const bool e2e_myshop = std::getenv("MT_E2E_MYSHOP") != nullptr;
|
|
const bool e2e_cube = std::getenv("MT_E2E_CUBE") != nullptr;
|
|
const bool e2e_sweep = std::getenv("MT_E2E_SWEEP") != nullptr;
|
|
const bool e2e_unsafe = std::getenv("MT_E2E_UNSAFE") != nullptr;
|
|
if (!game.connect(game_host, game_port)) {
|
|
std::printf("FAIL: game connect(): %s\n", game.last_error().c_str());
|
|
return 4;
|
|
}
|
|
start = clock_t_::now();
|
|
bool selected = false;
|
|
double select_t = 0;
|
|
bool tried_empire = false;
|
|
double loading_t = 0;
|
|
bool enter_sent = false;
|
|
int last_phase = -1;
|
|
while (elapsed(start) < 30.0) {
|
|
if (game.phase() == mtnet::PHASE_LOADING && loading_t == 0) {
|
|
loading_t = elapsed(start);
|
|
}
|
|
// give the server ~1.5s to finish the spawn burst, then say we're ready
|
|
if (game.phase() == mtnet::PHASE_LOADING && !enter_sent && loading_t > 0 &&
|
|
elapsed(start) - loading_t > 1.5) {
|
|
std::printf("[game] send CG_ENTERGAME (%.1fs into LOADING)\n", elapsed(start) - loading_t);
|
|
game.send_enter_game();
|
|
enter_sent = true;
|
|
}
|
|
game.world().set_now((uint32_t)(elapsed(start) * 1000.0));
|
|
game.process();
|
|
game.world().tick();
|
|
|
|
if ((int)game.phase() != last_phase) {
|
|
last_phase = (int)game.phase();
|
|
std::printf("[game] phase = %d (t=%.1fs)\n", last_phase, elapsed(start));
|
|
}
|
|
if (game.state() == mtnet::NetStream::State::Offline) {
|
|
std::printf("FAIL: game server dropped us (%s) at phase %d\n", game.last_error().c_str(),
|
|
(int)game.phase());
|
|
return 8;
|
|
}
|
|
if (game.char_list_ready() && !selected) {
|
|
std::printf("[game] char list: %zu slot(s) (empire byte=%d seen=%d)\n",
|
|
game.chars().size(), game.empire(), (int)game.empire_seen());
|
|
for (const auto &c : game.chars()) {
|
|
std::printf(" [%d] %-16s job=%d lv=%d (%d,%d)\n", c.index, c.name.c_str(),
|
|
c.job, c.level, c.x, c.y);
|
|
}
|
|
if (game.chars().empty()) {
|
|
std::printf("FAIL: no characters on the account\n");
|
|
return 5;
|
|
}
|
|
int idx = game.chars()[0].index;
|
|
for (const auto &c : game.chars()) {
|
|
if (c.index == char_index) {
|
|
idx = c.index;
|
|
}
|
|
}
|
|
|
|
// --- non-destructive create test: CG_CHARACTER_CREATE on an OCCUPIED
|
|
// slot must come back as GC_PLAYER_CREATE_FAILURE (nothing mutated).
|
|
if (e2e_charcreate) {
|
|
std::printf("[e2e] CG_CHARACTER_CREATE on occupied slot %d (expect FAILURE)\n", idx);
|
|
game.create_character(idx, "ZzTestName", 0, 0, 4, 3, 6, 3);
|
|
double t0 = elapsed(start);
|
|
bool got = false;
|
|
while (elapsed(start) - t0 < 3.0 && !got) {
|
|
game.process();
|
|
for (const auto &ev : game.drain_char_events()) {
|
|
using K = mtnet::GameClient::CharEvent::Kind;
|
|
if (ev.kind == K::CreateFail) {
|
|
std::printf("[e2e] -> GC_PLAYER_CREATE_FAILURE type=%d (struct OK)\n",
|
|
ev.fail_type);
|
|
got = true;
|
|
} else if (ev.kind == K::CreateOk) {
|
|
std::printf("[e2e] !! GC_PLAYER_CREATE_SUCCESS on occupied slot "
|
|
"(slot=%d) — unexpected, check slot choice\n",
|
|
ev.slot);
|
|
got = true;
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
if (!got) {
|
|
std::printf("[e2e] !! no create result in 3s (last_unknown=0x%04X) — "
|
|
"CG_CHARACTER_CREATE layout may be wrong\n",
|
|
game.last_unknown_header());
|
|
}
|
|
}
|
|
|
|
std::printf("[game] select_character(%d) -> %s\n", idx,
|
|
game.select_character(idx) ? "sent" : "SEND FAILED");
|
|
selected = true;
|
|
select_t = elapsed(start);
|
|
}
|
|
// stuck at SELECT >3s after selecting: try an explicit empire choice + reselect
|
|
if (selected && !tried_empire && game.phase() == mtnet::PHASE_SELECT &&
|
|
elapsed(start) - select_t > 3.0) {
|
|
std::printf("[game] still at SELECT %.1fs after select; sending CG_EMPIRE(1) + reselect\n",
|
|
elapsed(start) - select_t);
|
|
game.send_empire(1);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
game.process();
|
|
game.select_character(char_index);
|
|
tried_empire = true;
|
|
}
|
|
if (game.failed()) {
|
|
std::printf("FAIL: game (%s)\n", game.fail_reason().c_str());
|
|
return 6;
|
|
}
|
|
if (game.in_game()) {
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
|
}
|
|
if (!game.in_game()) {
|
|
std::printf("FAIL: never reached PHASE_GAME (phase=%d, last_unknown=0x%04X)\n",
|
|
(int)game.phase(), game.last_unknown_header());
|
|
return 7;
|
|
}
|
|
std::printf("[game] IN GAME (t=%.1fs)\n", elapsed(start));
|
|
|
|
// ---- phase 2.25: one-session protocol sweep ---------------------------
|
|
// These are deliberately sent one at a time with a short pump between them.
|
|
// A live socket only proves that the server accepted/framed the packet; a
|
|
// state/event change is printed separately as stronger evidence. Destructive
|
|
// requests (warp/dungeon/change-name/item mutation) stay behind MT_E2E_UNSAFE.
|
|
if (e2e_sweep) {
|
|
std::printf("\n=== one-session protocol sweep (safe subset) ===\n");
|
|
int sweep_sent = 0;
|
|
int sweep_alive = 0;
|
|
int sweep_events = 0;
|
|
auto pump_probe = [&](double seconds) {
|
|
auto p0 = clock_t_::now();
|
|
while (elapsed(p0) < seconds) {
|
|
game.world().set_now((uint32_t)(12000 + elapsed(p0) * 1000.0));
|
|
game.process();
|
|
game.world().tick();
|
|
if (game.state() == mtnet::NetStream::State::Offline) {
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
};
|
|
auto probe = [&](const char *name, uint16_t header, const std::function<bool()> &send,
|
|
const std::function<int()> &events = std::function<int()>()) {
|
|
const uint16_t unknown_before = game.last_unknown_header();
|
|
const bool sent = send();
|
|
if (sent) {
|
|
++sweep_sent;
|
|
}
|
|
pump_probe(0.45);
|
|
const bool alive = game.state() != mtnet::NetStream::State::Offline;
|
|
if (alive) {
|
|
++sweep_alive;
|
|
}
|
|
int event_count = events ? events() : 0;
|
|
sweep_events += event_count;
|
|
std::printf("[sweep] %-24s tx=0x%04X sent=%d alive=%d events=%d "
|
|
"unknown_before=0x%04X unknown_after=0x%04X\n",
|
|
name, header, (int)sent, (int)alive, event_count, unknown_before,
|
|
game.last_unknown_header());
|
|
};
|
|
const uint32_t self_vid = game.world().main_vid();
|
|
const mtnet::Entity *self = game.world().get(self_vid);
|
|
const int32_t self_x = self ? (int32_t)self->x : 0;
|
|
const int32_t self_y = self ? (int32_t)self->y : 0;
|
|
probe("CG_CHARACTER_POSITION", mtnet::CG_CHARACTER_POSITION,
|
|
[&]() { return game.send_character_position(0); });
|
|
mtnet::CGSyncPositionElement sync{};
|
|
sync.vid = self_vid;
|
|
sync.x = self_x;
|
|
sync.y = self_y;
|
|
probe("CG_SYNC_POSITION", mtnet::CG_SYNC_POSITION,
|
|
[&]() { return game.send_sync_positions({sync}); });
|
|
probe("CG_SCRIPT_SELECT_ITEM", mtnet::CG_SCRIPT_SELECT_ITEM,
|
|
[&]() { return game.send_script_select_item(0); },
|
|
[&]() { return (int)game.world().drain_scripts().size(); });
|
|
probe("CG_QUEST_CANCEL", mtnet::CG_QUEST_CANCEL,
|
|
[&]() { return game.send_quest_cancel(); },
|
|
[&]() { return (int)game.world().drain_quest_changes().size(); });
|
|
probe("CG_PARTY_USE_SKILL", mtnet::CG_PARTY_USE_SKILL,
|
|
[&]() { return game.send_party_use_skill(0, self_vid); });
|
|
probe("CG_FLY_TARGETING", mtnet::CG_FLY_TARGETING,
|
|
[&]() { return game.send_fly_targeting(self_vid, self_x, self_y); },
|
|
[&]() { return (int)game.world().drain_fly_target_cues().size(); });
|
|
probe("CG_ADD_FLY_TARGETING", mtnet::CG_ADD_FLY_TARGETING,
|
|
[&]() { return game.send_add_fly_targeting(self_vid, self_x, self_y); },
|
|
[&]() { return (int)game.world().drain_fly_target_cues().size(); });
|
|
probe("CG_FISHING", mtnet::CG_FISHING,
|
|
[&]() { return game.send_fishing(0); },
|
|
[&]() { return (int)game.world().drain_fishing_events().size(); });
|
|
probe("CG_SHOOT", mtnet::CG_SHOOT,
|
|
[&]() { return game.send_shoot(0); });
|
|
probe("CG_USE_SKILL", mtnet::CG_USE_SKILL,
|
|
[&]() { return game.send_use_skill(0, self_vid); });
|
|
if (e2e_unsafe) {
|
|
probe("CG_DUNGEON (unsafe)", mtnet::CG_DUNGEON,
|
|
[&]() { return game.send_dungeon(); },
|
|
[&]() { return (int)game.world().drain_dungeon_events().size(); });
|
|
probe("CG_WARP (unsafe)", mtnet::CG_WARP,
|
|
[&]() { return game.send_warp(); });
|
|
} else {
|
|
std::printf("[sweep] CG_DUNGEON/CG_WARP skipped (set MT_E2E_UNSAFE=1)\n");
|
|
}
|
|
std::printf("[sweep] summary sent=%d alive_after_probe=%d observed_events=%d "
|
|
"still_connected=%d\n",
|
|
sweep_sent, sweep_alive, sweep_events,
|
|
(int)(game.state() != mtnet::NetStream::State::Offline));
|
|
}
|
|
|
|
// ---- phase 2.5: exercise post-2026-08-30 protocols -------------------
|
|
std::printf("\n=== post-2026-08-30 protocol probes ===\n");
|
|
std::printf("[e2e] GC_EMPIRE: empire byte=%d seen=%d\n", game.empire(),
|
|
(int)game.empire_seen());
|
|
|
|
// cube: /cube rList <npc> is a plain chat command; server only answers when
|
|
// the player is actually at a cube NPC, so this mostly proves the send path.
|
|
if (e2e_cube) {
|
|
uint32_t cube_npc = 20383; // common blacksmith/cube NPC vnum
|
|
std::printf("[e2e] send '/cube rList %u'\n", cube_npc);
|
|
game.send_cube_result_list(cube_npc);
|
|
}
|
|
if (e2e_myshop) {
|
|
// open a 1-item shop from inventory cell 0 at a silly price, then close.
|
|
std::vector<mtnet::MyShopItem> items;
|
|
bool have_item = false;
|
|
for (int c = 0; c < mtnet::INVENTORY_MAX_NUM && !have_item; ++c) {
|
|
const mtnet::Item &it = game.world().inv_slot(c);
|
|
if (!it.empty()) {
|
|
mtnet::MyShopItem e{};
|
|
e.vnum = it.vnum;
|
|
e.count = it.count ? it.count : 1;
|
|
e.pos = {mtnet::WINDOW_INVENTORY, (uint16_t)c};
|
|
e.price = 99999999;
|
|
e.display_pos = 0;
|
|
items.push_back(e);
|
|
have_item = true;
|
|
std::printf("[e2e] CG_MYSHOP: 1 item (vnum=%u inv_cell=%d) price=99999999\n",
|
|
e.vnum, c);
|
|
}
|
|
}
|
|
if (!have_item) {
|
|
std::printf("[e2e] CG_MYSHOP: inventory empty, opening a 0-item shop\n");
|
|
}
|
|
game.send_open_private_shop("e2e test shop", items);
|
|
}
|
|
|
|
// pump ~3.5s so any server push (guild skill/war, shop broadcast) lands
|
|
{
|
|
auto p0 = clock_t_::now();
|
|
while (elapsed(p0) < 3.5) {
|
|
game.world().set_now((uint32_t)(15000 + elapsed(p0) * 1000.0));
|
|
game.process();
|
|
game.world().tick();
|
|
if (game.state() == mtnet::NetStream::State::Offline) {
|
|
std::printf("[e2e] !! DROPPED during probe pump: %s\n", game.last_error().c_str());
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
}
|
|
|
|
{
|
|
const mtnet::GuildSkillState &gs = game.world().guild_skill();
|
|
std::printf("[e2e] GUILD_GC_SKILL_INFO: valid=%d skill_point=%d guild_point=%d/%d "
|
|
"levels=[",
|
|
(int)gs.valid, gs.skill_point, gs.guild_point, gs.max_guild_point);
|
|
for (int i = 0; i < mtnet::GUILD_SKILL_MAX_NUM; ++i) {
|
|
std::printf("%d%s", gs.levels[i], i + 1 < mtnet::GUILD_SKILL_MAX_NUM ? "," : "");
|
|
}
|
|
std::printf("]\n");
|
|
const mtnet::GuildWarStatus &gw = game.world().guild_war();
|
|
std::printf("[e2e] GUILD_GC_WAR: state=%d type=%d opp_guild=%u active GvG pairs=%zu\n",
|
|
gw.state, gw.type, gw.opp_guild_id, game.world().guild_wars().size());
|
|
}
|
|
if (e2e_myshop) {
|
|
std::printf("[e2e] CG_MYSHOP: closing shop (SHOP_CG_END)\n");
|
|
game.send_close_private_shop();
|
|
auto p0 = clock_t_::now();
|
|
while (elapsed(p0) < 1.5) {
|
|
game.process();
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
for (const auto &err : game.world().drain_shop_errors()) {
|
|
std::printf("[e2e] shop error: %s\n", err.c_str());
|
|
}
|
|
}
|
|
std::printf("[e2e] cube state: open=%d npc=%u recipes=%zu results=%zu\n",
|
|
(int)game.world().cube().open, game.world().cube().npc_vnum,
|
|
game.world().cube().recipes.size(), game.world().cube().results.size());
|
|
std::printf("[e2e] mall state: open=%d size=%d\n", (int)game.world().mall_open(),
|
|
game.world().mall_size());
|
|
std::printf("[e2e] still connected after probes: %s\n",
|
|
game.state() == mtnet::NetStream::State::Offline ? "NO (dropped)" : "yes");
|
|
|
|
// ---- phase 3: observe + poke (move / attack with zero CRC) ---------
|
|
auto game_start = clock_t_::now();
|
|
bool poked = false;
|
|
while (elapsed(game_start) < run_secs) {
|
|
game.world().set_now((uint32_t)(20000 + elapsed(game_start) * 1000.0));
|
|
game.process();
|
|
game.world().tick();
|
|
if (game.state() == mtnet::NetStream::State::Offline) {
|
|
std::printf("[game] disconnected after %.1fs in game: %s\n", elapsed(game_start),
|
|
game.last_error().c_str());
|
|
break;
|
|
}
|
|
// 2s in: send a move + an attack (crc fields 0) and see if we survive
|
|
if (!poked && elapsed(game_start) > 2.0) {
|
|
const mtnet::Entity *me = game.world().get(game.world().main_vid());
|
|
int32_t x = me ? (int32_t)me->x : 0, y = me ? (int32_t)me->y : 0;
|
|
game.send_move(mtnet::FUNC_MOVE, 0, 0, x + 100, y, 2000);
|
|
game.send_attack(0, game.world().main_vid());
|
|
std::printf("[game] sent CG_MOVE + CG_ATTACK (crc=0) at +2s\n");
|
|
poked = true;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
if (game.state() != mtnet::NetStream::State::Offline) {
|
|
std::printf("[game] still connected at +%.0fs (zero-CRC attack accepted)\n", run_secs);
|
|
}
|
|
|
|
mtnet::EntityStore &w = game.world();
|
|
std::printf("\n=== snapshot after %.0fs in game ===\n", run_secs);
|
|
std::printf("main vid: %u\n", w.main_vid());
|
|
if (const mtnet::Entity *me = w.get(w.main_vid())) {
|
|
std::printf(" name=%s race=%u pos=(%.0f,%.0f,%.0f) hp=%d/%d level=%d\n", me->name.c_str(),
|
|
me->race, me->x, me->y, me->z, me->hp, me->max_hp, me->level);
|
|
}
|
|
std::printf("entities in view: %zu\n", w.size());
|
|
int named = 0, mounted = 0;
|
|
for (uint32_t vid : w.vids()) {
|
|
const mtnet::Entity *e = w.get(vid);
|
|
if (!e->name.empty() && vid != w.main_vid()) {
|
|
if (named++ < 10) {
|
|
std::printf(" NAMED vid=%-7u race=%-5u lvl=%-4d guild=%d pk=%d mount=%u \"%s\"\n",
|
|
vid, e->race, e->level, e->guild, e->pk_mode, e->mount_vnum,
|
|
e->name.c_str());
|
|
}
|
|
}
|
|
if (e->mount_vnum != 0) {
|
|
++mounted;
|
|
}
|
|
}
|
|
std::printf("named entities: %d mounted: %d\n", named, mounted);
|
|
const mtnet::PlayerPoints &p = w.points();
|
|
std::printf("points: hp=%d/%d sp=%d/%d level=%d exp=%d/%d gold=%d\n", p.hp(), p.max_hp(), p.sp(),
|
|
p.max_sp(), p.level(), p.exp(), p.next_exp(), p.gold());
|
|
int inv_n = 0;
|
|
for (int c = 0; c < mtnet::INVENTORY_MAX_NUM; ++c) {
|
|
if (!w.inv_slot(c).empty()) {
|
|
++inv_n;
|
|
}
|
|
}
|
|
std::printf("inventory: %d non-empty slot(s)\n", inv_n);
|
|
std::printf("party: %zu member(s) friends: %zu\n", w.party_pids().size(), w.friends().size());
|
|
int skn = 0, qsn = 0;
|
|
for (int i = 0; i < mtnet::SKILL_MAX_NUM; ++i) {
|
|
if (w.skill_level(i) > 0) {
|
|
++skn;
|
|
}
|
|
}
|
|
for (int i = 0; i < mtnet::QUICKSLOT_MAX_NUM; ++i) {
|
|
if (w.quickslot(i).type != 0) {
|
|
++qsn;
|
|
}
|
|
}
|
|
std::printf("skills known: %d quickslots set: %d\n", skn, qsn);
|
|
const mtnet::GuildState &gld = w.guild();
|
|
if (gld.in_guild) {
|
|
std::printf("guild: \"%s\" lvl %d members %d/%d gold %u (%zu in list)\n",
|
|
gld.name.c_str(), gld.level, gld.member_count, gld.max_member_count, gld.gold,
|
|
w.guild_members().size());
|
|
} else {
|
|
std::printf("guild: (none)\n");
|
|
}
|
|
for (int i = 0; i < mtnet::SKILL_MAX_NUM && skn > 0; ++i) {
|
|
if (w.skill_level(i) > 0) {
|
|
std::printf(" skill %d: lvl %d master %d\n", i, w.skill_level(i), w.skill_master(i));
|
|
if (--skn == 0 || i > 60) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::printf("\nRESULT: e2e OK — reached PHASE_GAME and pumped %.0fs\n", run_secs);
|
|
return 0;
|
|
}
|