- 桥梁与静态物体高度采样修复: - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程 - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题 - 新增 test_bridge_height_parity.gd 自动化对拍测试 - 40250 怪物击杀经验动效: - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附 - 40250 客户端全系统功能对齐(Batches 1-31): - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试 - 文档沉淀: - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
201 lines
7.7 KiB
C++
201 lines
7.7 KiB
C++
// 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 <host> <port> <id> <password> [slot] [timeout_seconds]
|
|
// MT_CLASSIC_AUTH=1 selects the 40250 auth(11000) -> game(<port>) flow.
|
|
//
|
|
// 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 <chrono>
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
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<std::chrono::milliseconds>(steady_clock_t::now() - start)
|
|
.count() /
|
|
1000.0;
|
|
}
|
|
|
|
static int choose_slot(const std::vector<mtnet::classic::CharSlot> &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 <host> <port> <id> <password> [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;
|
|
const int enter_delay_ms = std::getenv("MT_CLASSIC_ENTER_DELAY") ?
|
|
std::atoi(std::getenv("MT_CLASSIC_ENTER_DELAY")) : 8000;
|
|
session.set_auto_entergame_delay(enter_delay_ms);
|
|
session.set_wire_trace(std::getenv("MT_NET_TRACE") != nullptr &&
|
|
std::string(std::getenv("MT_NET_TRACE")) == "1");
|
|
const bool use_auth = std::getenv("MT_CLASSIC_AUTH") != nullptr;
|
|
const bool use_direct_enter = std::getenv("MT_CLASSIC_DIRECT") != nullptr;
|
|
const int auth_port = std::getenv("MT_CLASSIC_AUTH_PORT") ?
|
|
std::atoi(std::getenv("MT_CLASSIC_AUTH_PORT")) : 11000;
|
|
const bool connected = use_auth ?
|
|
session.connect(host, static_cast<uint16_t>(auth_port), host, static_cast<uint16_t>(port),
|
|
id, password) :
|
|
session.connect(host, static_cast<uint16_t>(port), id, password);
|
|
if (!connected) {
|
|
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<uint32_t>(elapsed_seconds(start) * 1000.0);
|
|
session.set_now(now);
|
|
session.pump();
|
|
|
|
const int current_stage = static_cast<int>(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 pos=(%d,%d) main_part=%u hair_part=%u change_name=%d\n",
|
|
i, slot.name.c_str(), slot.id, slot.level, slot.x, slot.y, slot.main_part,
|
|
slot.hair_part, (int)slot.change_name);
|
|
}
|
|
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;
|
|
}
|
|
const bool enter_ok = use_direct_enter ? session.connect_direct_enter(slot) : session.select_char(slot);
|
|
if (!enter_ok) {
|
|
std::fprintf(stderr, "FAIL: %s(%d): %s\n",
|
|
use_direct_enter ? "connect_direct_enter" : "select_char", slot,
|
|
session.last_error().c_str());
|
|
session.disconnect();
|
|
return 4;
|
|
}
|
|
std::printf("[e2e] %s slot=%d\n",
|
|
use_direct_enter ? "direct-entered" : "selected", 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();
|
|
std::array<int, 10> entity_types{};
|
|
for (uint32_t vid : world.vids()) {
|
|
if (const auto *entity = world.get(vid); entity && entity->ch_type < entity_types.size()) {
|
|
++entity_types[entity->ch_type];
|
|
}
|
|
}
|
|
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 skill_group=%u\n",
|
|
world.main_vid(), world.size(), inventory_count, world.points().level(),
|
|
(unsigned)world.skill_group());
|
|
std::printf(" entity_types pc=%d npc=%d monster=%d stone=%d warp=%d door=%d building=%d horse=%d goto=%d\n",
|
|
entity_types[mtnet::classic::CHRTYPE_PC], entity_types[mtnet::classic::CHRTYPE_NPC],
|
|
entity_types[mtnet::classic::CHRTYPE_MONSTER], entity_types[mtnet::classic::CHRTYPE_STONE],
|
|
entity_types[mtnet::classic::CHRTYPE_WARP], entity_types[mtnet::classic::CHRTYPE_DOOR],
|
|
entity_types[mtnet::classic::CHRTYPE_BUILDING], entity_types[mtnet::classic::CHRTYPE_HORSE],
|
|
entity_types[mtnet::classic::CHRTYPE_GOTO]);
|
|
if (const mtnet::Entity *me = world.get(world.main_vid())) {
|
|
std::printf(" main race=%u pos_cm=(%.0f,%.0f,%.0f) parts=[%u,%u,%u,%u] name=%s\n",
|
|
me->race, me->x, me->y, me->z, me->parts[0], me->parts[1], me->parts[2],
|
|
me->parts[3], me->name.c_str());
|
|
}
|
|
for (int wear = 0; wear < mtnet::WEAR_MAX_NUM; ++wear) {
|
|
const mtnet::Item &item = world.equip_slot(wear);
|
|
if (!item.empty()) {
|
|
std::printf(" equip wear=%d vnum=%u count=%u\n", wear, item.vnum,
|
|
(unsigned)item.count);
|
|
}
|
|
}
|
|
for (int pos = 0; pos < mtnet::QUICKSLOT_MAX_NUM; ++pos) {
|
|
const mtnet::QuickSlot slot = world.quickslot(pos);
|
|
if (slot.type != 0) {
|
|
std::printf(" quickslot pos=%d type=%u ref=%u\n", pos,
|
|
(unsigned)slot.type, (unsigned)slot.position);
|
|
}
|
|
}
|
|
for (int id = 0; id < mtnet::SKILL_MAX_NUM; ++id) {
|
|
if (world.skill_level(id) != 0) {
|
|
std::printf(" skill id=%d level=%u master=%u\n", id,
|
|
(unsigned)world.skill_level(id), (unsigned)world.skill_master(id));
|
|
}
|
|
}
|
|
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;
|
|
}
|