fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
// 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.
|
||||
// It intentionally stops after PHASE_GAME, unless MT_CLASSIC_WARP_COMMAND is
|
||||
// set. The optional command is sent once after the first PHASE_GAME and the
|
||||
// probe then requires a second PHASE_GAME, allowing a reversible GM /warp
|
||||
// command to exercise the 40250 GC_WARP replacement transport.
|
||||
//
|
||||
// net_classic_e2e <host> <port> <id> <password> [slot] [timeout_seconds]
|
||||
// MT_CLASSIC_AUTH=1 selects the 40250 auth(11000) -> game(<port>) flow.
|
||||
// MT_CLASSIC_WARP_COMMAND="/warp <x> <y>" performs one GC_WARP round trip.
|
||||
// MT_CLASSIC_EXPECT_EMPTY=1 verifies an all-empty SelectPhase character list
|
||||
// without trying to select a stale slot or entering the game.
|
||||
//
|
||||
// Exit status 0 means the socket handshake, DH2/CTR negotiation, login,
|
||||
// character list, character selection and PHASE_GAME were all reached.
|
||||
// Exit status 5 means an application-level login/game-connect failure callback
|
||||
// was observed; 7 means an established game peer disconnected.
|
||||
|
||||
#include "../src/net/classic/classic_session.h"
|
||||
|
||||
@@ -72,13 +77,35 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
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);
|
||||
bool login_failed = false;
|
||||
std::string login_failure_reason;
|
||||
bool remote_disconnected = false;
|
||||
std::string remote_disconnect_reason;
|
||||
// LoginPhase/AccountConnector reports an application-level rejection to the
|
||||
// login UI while retaining the phase owner. A live probe must observe that
|
||||
// callback instead of waiting for a terminal Stage::Failed (which 40250 does
|
||||
// not enter for WRONGPWD/BESAMEKEY).
|
||||
session.on_login_failure = [&](const std::string &reason) {
|
||||
login_failed = true;
|
||||
login_failure_reason = reason;
|
||||
};
|
||||
// A connected game peer close is routed through SetLoginPhase by the
|
||||
// reference client. The probe stops at the first notification so a retry
|
||||
// packet cannot hide the original live failure in the report.
|
||||
session.on_remote_disconnect = [&](const std::string &reason) {
|
||||
remote_disconnected = true;
|
||||
remote_disconnect_reason = reason;
|
||||
};
|
||||
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 bool expect_empty = std::getenv("MT_CLASSIC_EXPECT_EMPTY") != nullptr &&
|
||||
std::string(std::getenv("MT_CLASSIC_EXPECT_EMPTY")) == "1";
|
||||
const char *warp_command_env = std::getenv("MT_CLASSIC_WARP_COMMAND");
|
||||
const std::string warp_command = warp_command_env ? warp_command_env : "";
|
||||
const int requested_empire = std::getenv("MT_CLASSIC_EMPIRE") ?
|
||||
std::atoi(std::getenv("MT_CLASSIC_EMPIRE")) : 0;
|
||||
const int auth_port = std::getenv("MT_CLASSIC_AUTH_PORT") ?
|
||||
std::atoi(std::getenv("MT_CLASSIC_AUTH_PORT")) : 11000;
|
||||
const bool connected = use_auth ?
|
||||
@@ -95,17 +122,49 @@ int main(int argc, char **argv) {
|
||||
auto start = steady_clock_t::now();
|
||||
int last_stage = -1;
|
||||
bool selected = false;
|
||||
bool empire_selected = false;
|
||||
bool warp_requested = false;
|
||||
bool warp_left_game = 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();
|
||||
|
||||
if (login_failed) {
|
||||
std::fprintf(stderr, "FAIL login callback: %s\n",
|
||||
login_failure_reason.empty() ? "unknown" : login_failure_reason.c_str());
|
||||
session.disconnect();
|
||||
return 5;
|
||||
}
|
||||
if (remote_disconnected) {
|
||||
std::fprintf(stderr, "FAIL remote disconnect: %s\n",
|
||||
remote_disconnect_reason.empty() ? "disconnected" :
|
||||
remote_disconnect_reason.c_str());
|
||||
session.disconnect();
|
||||
return 7;
|
||||
}
|
||||
|
||||
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 && !empire_selected &&
|
||||
requested_empire >= 1 && requested_empire <= 3 &&
|
||||
session.parser().empire() == 0) {
|
||||
const bool sent = session.send_empire(static_cast<uint8_t>(requested_empire));
|
||||
std::printf("[e2e] select empire=%d -> %s\n", requested_empire,
|
||||
sent ? "sent" : "SEND FAILED");
|
||||
if (!sent) {
|
||||
std::fprintf(stderr, "FAIL: select empire: %s\n",
|
||||
session.last_error().c_str());
|
||||
session.disconnect();
|
||||
return 4;
|
||||
}
|
||||
empire_selected = true;
|
||||
}
|
||||
|
||||
if (session.stage() == mtnet::INetSession::Stage::CharSelect && !selected &&
|
||||
session.parser().char_list_ready()) {
|
||||
const auto &slots = session.char_slots();
|
||||
@@ -113,8 +172,24 @@ int main(int argc, char **argv) {
|
||||
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);
|
||||
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 first_selectable = choose_slot(slots, -1);
|
||||
if (expect_empty) {
|
||||
if (first_selectable >= 0) {
|
||||
std::fprintf(stderr,
|
||||
"FAIL: MT_CLASSIC_EXPECT_EMPTY=1 but slot %d is occupied\n",
|
||||
first_selectable);
|
||||
session.disconnect();
|
||||
return 3;
|
||||
}
|
||||
// ClientVS22 accepts a zero-filled LOGIN_SUCCESS3/4 payload and
|
||||
// opens SelectPhase; an empty account is not a login failure. Stop
|
||||
// here so the probe never mutates the account or invents a slot.
|
||||
std::printf("PASS EMPTY CHARACTER LIST PHASE_SELECT slots=%zu\n", slots.size());
|
||||
session.disconnect();
|
||||
return 0;
|
||||
}
|
||||
const int slot = choose_slot(slots, requested_slot);
|
||||
if (slot < 0) {
|
||||
@@ -140,7 +215,41 @@ int main(int argc, char **argv) {
|
||||
session.disconnect();
|
||||
return 5;
|
||||
}
|
||||
if (warp_requested && session.stage() != mtnet::INetSession::Stage::InGame) {
|
||||
warp_left_game = true;
|
||||
}
|
||||
if (!warp_command.empty()) {
|
||||
mtnet::WarpCue warp{};
|
||||
if (session.world().take_next_warp(warp)) {
|
||||
const uint32_t addr = static_cast<uint32_t>(warp.addr);
|
||||
char warp_host[16];
|
||||
std::snprintf(warp_host, sizeof(warp_host), "%u.%u.%u.%u", addr & 0xFF,
|
||||
(addr >> 8) & 0xFF, (addr >> 16) & 0xFF, (addr >> 24) & 0xFF);
|
||||
if (!session.connect_warp(warp_host, warp.port)) {
|
||||
std::fprintf(stderr, "FAIL: GC_WARP reconnect: %s\n",
|
||||
session.last_error().c_str());
|
||||
session.disconnect();
|
||||
return 4;
|
||||
}
|
||||
std::printf("[e2e] GC_WARP reconnect -> %s:%u\n", warp_host,
|
||||
(unsigned)warp.port);
|
||||
}
|
||||
}
|
||||
if (session.stage() == mtnet::INetSession::Stage::InGame) {
|
||||
if (!warp_command.empty() && !warp_requested) {
|
||||
if (!session.send_chat(0, warp_command)) {
|
||||
std::fprintf(stderr, "FAIL: GC_WARP command send: %s\n",
|
||||
session.last_error().c_str());
|
||||
session.disconnect();
|
||||
return 4;
|
||||
}
|
||||
warp_requested = true;
|
||||
std::printf("[e2e] GC_WARP command sent\n");
|
||||
}
|
||||
if (!warp_command.empty() && (!warp_requested || !warp_left_game)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
||||
continue;
|
||||
}
|
||||
const auto &world = session.world();
|
||||
std::array<int, 10> entity_types{};
|
||||
for (uint32_t vid : world.vids()) {
|
||||
@@ -152,7 +261,8 @@ int main(int argc, char **argv) {
|
||||
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",
|
||||
std::printf("PASS PHASE_GAME%s main_vid=%u entities=%zu inventory_slots=%d points_level=%d skill_group=%u\n",
|
||||
warp_command.empty() ? "" : " (GC_WARP round trip)",
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user