// net_classic_e2e — minimal live probe for the 40250 classic backend. // // It intentionally stops after PHASE_GAME. The login/selection/world burst // is deterministic and safe to run against a test account; social, storage, // mall, NPC and cube mutations require a configured account/NPC fixture and // are driven by the Godot UI or the documented command surface. // // net_classic_e2e [slot] [timeout_seconds] // // Exit status 0 means the socket handshake, DH2/CTR negotiation, login, // character list, character selection and PHASE_GAME were all reached. #include "../src/net/classic/classic_session.h" #include #include #include #include #include using steady_clock_t = std::chrono::steady_clock; static const char *stage_name(mtnet::INetSession::Stage stage) { using Stage = mtnet::INetSession::Stage; switch (stage) { case Stage::Offline: return "offline"; case Stage::Connecting: return "connecting"; case Stage::LoggingIn: return "login"; case Stage::CharSelect: return "char_select"; case Stage::Loading: return "loading"; case Stage::InGame: return "in_game"; case Stage::Failed: return "failed"; } return "unknown"; } static double elapsed_seconds(steady_clock_t::time_point start) { return std::chrono::duration_cast(steady_clock_t::now() - start) .count() / 1000.0; } static int choose_slot(const std::vector &slots, int requested) { if (requested >= 0 && requested < (int)slots.size() && !slots[requested].empty()) { return requested; } for (int i = 0; i < (int)slots.size(); ++i) { if (!slots[i].empty()) return i; } return -1; } int main(int argc, char **argv) { if (argc < 5 || argc > 7) { std::fprintf(stderr, "usage: %s [slot] [timeout_seconds]\n", argv[0]); return 64; } const std::string host = argv[1]; const int port = std::atoi(argv[2]); const std::string id = argv[3]; const std::string password = argv[4]; const int requested_slot = argc >= 6 ? std::atoi(argv[5]) : 0; const double timeout = argc >= 7 ? std::atof(argv[6]) : 30.0; if (port <= 0 || port > 65535 || timeout <= 0.0) { std::fprintf(stderr, "invalid port or timeout\n"); return 64; } mtnet::classic::ClassicSession session; session.set_auto_entergame_delay(1500); session.set_wire_trace(std::getenv("MT_NET_TRACE") != nullptr); if (!session.connect(host, static_cast(port), id, password)) { std::fprintf(stderr, "FAIL connect: %s\n", session.last_error().c_str()); return 2; } std::printf("classic e2e -> %s:%d account=%s requested_slot=%d\n", host.c_str(), port, id.c_str(), requested_slot); auto start = steady_clock_t::now(); int last_stage = -1; bool selected = false; while (elapsed_seconds(start) < timeout) { const uint32_t now = static_cast(elapsed_seconds(start) * 1000.0); session.set_now(now); session.pump(); const int current_stage = static_cast(session.stage()); if (current_stage != last_stage) { last_stage = current_stage; std::printf("[%.1fs] stage=%s\n", elapsed_seconds(start), stage_name(session.stage())); } if (session.stage() == mtnet::INetSession::Stage::CharSelect && !selected && session.parser().char_list_ready()) { const auto &slots = session.char_slots(); std::printf("[e2e] character slots=%zu\n", slots.size()); for (size_t i = 0; i < slots.size(); ++i) { const auto &slot = slots[i]; std::printf(" [%zu] %s id=%u level=%u\n", i, slot.name.c_str(), slot.id, slot.level); } const int slot = choose_slot(slots, requested_slot); if (slot < 0) { std::fprintf(stderr, "FAIL: account has no selectable character\n"); session.disconnect(); return 3; } if (!session.select_char(slot)) { std::fprintf(stderr, "FAIL: select_char(%d)\n", slot); session.disconnect(); return 4; } std::printf("[e2e] selected slot=%d\n", slot); selected = true; } if (session.stage() == mtnet::INetSession::Stage::Failed) { std::fprintf(stderr, "FAIL stage: %s\n", session.last_error().c_str()); session.disconnect(); return 5; } if (session.stage() == mtnet::INetSession::Stage::InGame) { const auto &world = session.world(); int inventory_count = 0; for (int cell = 0; cell < mtnet::INVENTORY_MAX_NUM; ++cell) { if (!world.inv_slot(cell).empty()) ++inventory_count; } std::printf("PASS PHASE_GAME main_vid=%u entities=%zu inventory_slots=%d points_level=%d\n", world.main_vid(), world.size(), inventory_count, world.points().level()); session.disconnect(); return 0; } std::this_thread::sleep_for(std::chrono::milliseconds(15)); } std::fprintf(stderr, "FAIL timeout after %.1fs at stage=%s: %s\n", timeout, stage_name(session.stage()), session.last_error().c_str()); session.disconnect(); return 6; }