feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复

- 桥梁与静态物体高度采样修复:
  - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+31 -3
View File
@@ -14,6 +14,7 @@
#include "../src/net/classic/classic_session.h"
#include <chrono>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <string>
@@ -74,7 +75,8 @@ int main(int argc, char **argv) {
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);
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") ?
@@ -140,12 +142,25 @@ int main(int argc, char **argv) {
}
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\n",
world.main_vid(), world.size(), inventory_count, world.points().level());
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],
@@ -158,6 +173,19 @@ int main(int argc, char **argv) {
(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;
}