客户端问题修改

This commit is contained in:
shen
2026-09-07 14:53:36 +08:00
parent b19d5b5dd3
commit 61272b75e9
32 changed files with 1023 additions and 95 deletions
+16
View File
@@ -68,6 +68,7 @@ void Metin2Model::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_surface_texture", "surface", "path"), &Metin2Model::set_surface_texture);
ClassDB::bind_method(D_METHOD("reload"), &Metin2Model::reload);
ClassDB::bind_method(D_METHOD("get_info"), &Metin2Model::get_info);
ClassDB::bind_method(D_METHOD("get_ground_offset"), &Metin2Model::get_ground_offset);
ClassDB::bind_method(D_METHOD("get_hair_options"), &Metin2Model::get_hair_options);
ClassDB::bind_method(D_METHOD("set_hair_gr2", "p"), &Metin2Model::set_hair_gr2);
ClassDB::bind_method(D_METHOD("get_hair_gr2"), &Metin2Model::get_hair_gr2);
@@ -455,6 +456,10 @@ void Metin2Model::reload() {
base_parts = parts;
AABB bounds;
Ref<ArrayMesh> mesh = build_mesh(fi, parts, flip_winding, bounds);
// build_mesh keeps the source GR2 coordinate frame (Z-up, centimetres);
// make_conv() below maps it to Godot Y-up and applies unit_scale. Keep the
// actor root on the terrain while translating only the visual subtree.
ground_offset = -static_cast<double>(bounds.position.z) * unit_scale;
if (mesh->get_surface_count() > 0) {
mi = memnew(MeshInstance3D);
mi->set_name("MeshInstance3D");
@@ -1085,6 +1090,17 @@ void Metin2Model::_apply_materials() {
return _load_dds(dir.path_join(dds_files[i]));
}
}
// Expansion armors share face textures with the base PC pack.
// Resolve that exact name before falling back to unrelated local DDS.
String cls = gr2_path.get_base_dir().get_file();
for (const String &pc : { String("pc"), String("pc2") }) {
String spec = "d:/ymir work/" + pc + "/" + cls + "/" + want + ".dds";
String resolved = resolve_rel_gr2(gr2_path, spec);
Ref<ImageTexture> shared = _load_dds(resolved);
if (shared.is_valid()) {
return shared;
}
}
return Ref<ImageTexture>();
};
+4
View File
@@ -126,6 +126,9 @@ public:
// One-line summary (bones / meshes / verts / bounds).
godot::String get_info() const;
// Parent-space Y adjustment that places the bind-pose model's lowest point
// on the actor origin. GR2 is Z-up/cm; this is converted to Godot Y/metres.
double get_ground_offset() const { return ground_offset; }
// --- C++ accessors for Metin2AnimPlayer (same extension) ---
const gr2::Skeleton *gr2_skeleton() const;
@@ -210,6 +213,7 @@ private:
godot::Skeleton3D *skel = nullptr;
godot::MeshInstance3D *mi = nullptr;
godot::String last_info;
double ground_offset = 0.0;
godot::HashMap<godot::String, godot::Ref<godot::ImageTexture>> tex_cache;
// §2.10 LOD
@@ -1497,6 +1497,15 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
return true;
}
case HDR_GC_QUEST_SCRIPT:
// Optional extended quest UI data. Consume it even when the UI is absent.
return true;
case HDR_GC_EXTENDED_STATUS:
// Optional server status payload; framing is sufficient for connection safety.
return true;
case HDR_GC_EXTENDED_CHARACTER:
// Optional extended character payload; consume without requiring its UI.
return true;
default:
m_last_error = "phase " + m_phase_name + " does not handle GC header " +
std::to_string(header);
+49 -1
View File
@@ -21,13 +21,55 @@ std::string ipv4_from_slot_addr(int32_t addr) {
std::to_string((value >> 16) & 0xffu) + "." +
std::to_string((value >> 24) & 0xffu);
}
ClassicStream::RawPacketResult consume_mark_packet(uint8_t header, const uint8_t *packet,
size_t available) {
// 40250 guild-mark packets use a one-byte header followed by a u32 whole
// packet size. They can be pushed on the game connection while the map is
// loading, so they must be drained before the normal GC static-size table is
// consulted. The dedicated mark client owns decoding; the game session only
// needs to preserve framing here.
size_t head_size = 0;
if (header == HDR_GC_MARK_IDXLIST) {
head_size = sizeof(GCMarkIDXList);
} else if (header == HDR_GC_MARK_BLOCK) {
head_size = sizeof(GCMarkBlock);
} else {
return {ClassicStream::RawPacketStatus::NotHandled, 0};
}
if (available < head_size) {
return {ClassicStream::RawPacketStatus::NeedMore, 0};
}
uint32_t total = 0;
std::memcpy(&total, packet + 1, sizeof(total));
constexpr uint32_t kMaxMarkPacket = 4u * 1024u * 1024u;
// Some 40250 builds emit an empty mark response as just the fixed head
// ([buf_size]=0, [count]=0) instead of filling buf_size with head_size.
// Consume that ten/seven-byte marker so the following GC packet remains
// aligned.
if (total == 0) {
return {ClassicStream::RawPacketStatus::Consumed, head_size};
}
if (total < head_size || total > kMaxMarkPacket) {
return {ClassicStream::RawPacketStatus::Error, 0};
}
if (available < total) {
return {ClassicStream::RawPacketStatus::NeedMore, 0};
}
return {ClassicStream::RawPacketStatus::Consumed, total};
}
} // namespace
ClassicSession::ClassicSession() : m_parser(m_world) {
m_stream.set_trace_name("game");
m_auth_stream.set_trace_name("auth");
m_stream.on_phase = [this](uint8_t p) { on_phase(p); };
m_stream.on_packet = [this](uint8_t h, const uint8_t *b, uint32_t l) {
return on_packet(h, b, l);
};
m_stream.on_raw_packet = [](uint8_t h, const uint8_t *packet, size_t available) {
return consume_mark_packet(h, packet, available);
};
m_stream.on_error = [this](const std::string &e) {
if (m_auth_transition) {
return;
@@ -108,6 +150,7 @@ void ClassicSession::start_offline(const std::string &id, const std::string &pw)
m_auth_login_sent = false;
m_login_sent = false;
m_entergame_sent = false;
m_loading_phase_seen = false;
m_version_sent = false;
m_game_connection_seen = false;
m_direct_enter = false;
@@ -206,6 +249,7 @@ bool ClassicSession::connect_direct_enter(int slot) {
m_game_port = character.port;
m_login_sent = false;
m_entergame_sent = false;
m_loading_phase_seen = false;
m_version_sent = false;
m_stream.set_time_sync_mode(false);
m_auth_transition = true;
@@ -242,6 +286,7 @@ bool ClassicSession::connect_warp(const std::string &game_host, uint16_t game_po
m_game_port = game_port;
m_login_sent = false;
m_entergame_sent = false;
m_loading_phase_seen = false;
m_version_sent = false;
m_stream.set_time_sync_mode(false);
m_auth_transition = true;
@@ -276,6 +321,7 @@ void ClassicSession::pump() {
m_game_started = true;
m_login_sent = false;
m_entergame_sent = false;
m_loading_phase_seen = false;
m_version_sent = false;
m_stream.set_time_sync_mode(false);
m_auth_transition = true;
@@ -293,7 +339,8 @@ void ClassicSession::pump() {
}
// auto CG_ENTERGAME a beat into PHASE_LOADING (the m2dev client waits ~1.5s;
// sending it too early makes the server drop us mid spawn-burst).
if (m_stage == Stage::Loading && !m_entergame_sent && m_entergame_delay > 0 &&
if (m_stage == Stage::Loading && m_loading_phase_seen && !m_entergame_sent &&
m_entergame_delay > 0 &&
m_now - m_loading_since >= m_entergame_delay) {
if (!enter_game()) {
m_last_error = m_stream.last_error().empty() ? "failed to send CG_ENTERGAME" :
@@ -536,6 +583,7 @@ void ClassicSession::on_phase(uint8_t phase) {
break;
case PHASE_LOADING:
set_stage(Stage::Loading);
m_loading_phase_seen = true;
m_direct_enter = false;
m_direct_enter_slot = -1;
m_loading_since = m_now;
@@ -204,6 +204,10 @@ private:
uint32_t m_now = 0;
uint32_t m_loading_since = 0;
uint32_t m_entergame_delay = 8000;
// DirectEnter reports a UI loading state while the server is still in
// PHASE_SELECT. Keep the auto-enter timer gated on the actual
// PHASE_LOADING packet so CG_ENTERGAME is never sent early.
bool m_loading_phase_seen = false;
bool m_entergame_sent = false;
bool m_login_sent = false;
bool m_auth_login_sent = false;
+44 -7
View File
@@ -235,7 +235,7 @@ bool ClassicStream::send_fixed(const void *struct_bytes, size_t n) {
emit_bytes(struct_bytes, n);
append_sequence_if_needed(header);
if (m_trace) {
std::fprintf(stderr, "[classic] send hdr=%u n=%zu seq=%d\n", header, n,
std::fprintf(stderr, "[classic:%s] send hdr=%u n=%zu seq=%d\n", m_trace_name.c_str(), header, n,
(m_seq_on && is_sequence_cg(header)));
}
return true;
@@ -284,7 +284,7 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
on_server_handshake();
}
if (m_trace) {
std::fprintf(stderr, "[classic] handshake echo time=%u\n", hs.time);
std::fprintf(stderr, "[classic:%s] handshake echo time=%u\n", m_trace_name.c_str(), hs.time);
}
return true;
}
@@ -309,7 +309,7 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
consumed = true;
m_phase = p.phase;
if (m_trace) {
std::fprintf(stderr, "[classic] phase -> %u\n", p.phase);
std::fprintf(stderr, "[classic:%s] phase -> %u\n", m_trace_name.c_str(), p.phase);
}
if (on_phase) {
on_phase(p.phase);
@@ -357,7 +357,7 @@ bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
m_cipher.decrypt(m_recv.mutable_unread(), m_recv.readable());
}
if (m_trace) {
std::fprintf(stderr, "[classic] cipher activated\n");
std::fprintf(stderr, "[classic:%s] cipher activated\n", m_trace_name.c_str());
}
if (on_cipher_active) {
on_cipher_active();
@@ -403,7 +403,7 @@ bool ClassicStream::handle_key_agreement() {
// reply with our blob — still plaintext (peer activates on its own COMPLETED)
emit_bytes(&out, sizeof(out));
if (m_trace) {
std::fprintf(stderr, "[classic] key agreement: replied, keys ready\n");
std::fprintf(stderr, "[classic:%s] key agreement: replied, keys ready\n", m_trace_name.c_str());
}
return true;
}
@@ -493,7 +493,7 @@ void ClassicStream::dispatch() {
uint32_t body_len = dh.size - sizeof(DynHead);
const uint8_t *body = m_recv.read_ptr();
if (m_trace) {
std::fprintf(stderr, "[classic] recv hdr=%u n=%u dynamic=1\n", header, dh.size);
std::fprintf(stderr, "[classic:%s] recv hdr=%u n=%u dynamic=1\n", m_trace_name.c_str(), header, dh.size);
}
bool ok = !on_packet || on_packet(header, body, body_len);
discard_recv(body_len);
@@ -517,6 +517,34 @@ void ClassicStream::dispatch() {
m_last_error = "unknown GC header " + std::to_string(header) +
" (last: " + std::to_string(m_last_pkt[0]) + "," +
std::to_string(m_last_pkt[1]) + ")";
if (m_trace) {
std::fprintf(stderr, "[classic:%s] unknown hdr=%u buffered=%zu bytes:",
m_trace_name.c_str(), header, m_recv.readable());
const size_t dump = std::min<size_t>(m_recv.readable(), 32);
for (size_t i = 0; i < dump; ++i) {
std::fprintf(stderr, " %02x", m_recv.read_ptr()[i]);
}
std::fprintf(stderr, "\n");
if (m_recv.readable() >= 3) {
uint16_t candidate_size = 0;
std::memcpy(&candidate_size, m_recv.read_ptr() + 1, sizeof(candidate_size));
const int after = candidate_size < m_recv.readable() ? m_recv.read_ptr()[candidate_size] : -1;
if (after < 0) {
std::fprintf(stderr, "[classic:%s] unknown candidate_size=%u after=none\n",
m_trace_name.c_str(), candidate_size);
} else {
std::fprintf(stderr, "[classic:%s] unknown candidate_size=%u after=0x%02x\n",
m_trace_name.c_str(), candidate_size, after);
}
if (candidate_size >= 3 && candidate_size <= m_recv.readable() && candidate_size <= 2048) {
std::fprintf(stderr, "[classic:%s] unknown candidate bytes:", m_trace_name.c_str());
for (size_t i = 0; i < candidate_size; ++i) {
std::fprintf(stderr, " %02x", m_recv.read_ptr()[i]);
}
std::fprintf(stderr, "\n");
}
}
}
if (on_error) {
on_error(m_last_error);
}
@@ -528,7 +556,16 @@ void ClassicStream::dispatch() {
}
const uint8_t *p = m_recv.read_ptr();
if (m_trace) {
std::fprintf(stderr, "[classic] recv hdr=%u n=%d dynamic=0\n", header, total);
std::fprintf(stderr, "[classic:%s] recv hdr=%u n=%d dynamic=0\n", m_trace_name.c_str(), header, total);
if (header == HDR_GC_LOGIN_SUCCESS_NEWSLOT || header == HDR_GC_SKILL_COOLTIME_END) {
const int next = m_recv.readable() > static_cast<size_t>(total) ? p[total] : -1;
if (next < 0) {
std::fprintf(stderr, "[classic:%s] boundary hdr=%u next=none\n", m_trace_name.c_str(), header);
} else {
std::fprintf(stderr, "[classic:%s] boundary hdr=%u next=0x%02x\n", m_trace_name.c_str(), header,
next);
}
}
}
bool ok = !on_packet || on_packet(header, p + 1, static_cast<uint32_t>(total - 1));
discard_recv(static_cast<size_t>(total));
@@ -23,6 +23,7 @@
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
namespace mtnet::classic {
@@ -94,6 +95,7 @@ public:
void set_time_sync_mode(bool on) { m_time_sync_mode = on; }
void set_wire_trace(bool on) { m_trace = on; }
void set_trace_name(std::string name) { m_trace_name = std::move(name); }
// _IMPROVED_PACKET_ENCRYPTION_ (docs §3). Client polarity = true.
bool cipher_active() const { return m_cipher.activated(); }
@@ -135,6 +137,7 @@ private:
bool m_handshake_seen = false;
bool m_time_sync_mode = false;
bool m_trace = false;
std::string m_trace_name = "stream";
uint8_t m_phase = PHASE_HANDSHAKE;
uint8_t m_last_pkt[2] = {0, 0};
+9
View File
@@ -276,6 +276,12 @@ enum : uint8_t {
HDR_GC_EMPIRE = 90,
HDR_GC_PARTY_LINK = 91,
HDR_GC_PARTY_UNLINK = 92,
// 40250 extended quest/script notification; payload is dynamically framed.
HDR_GC_QUEST_SCRIPT = 148,
// Extended servers also emit a dynamically framed mount/status notice here.
HDR_GC_EXTENDED_STATUS = 59,
// 40250 extended character/status packet (dynamic framing).
HDR_GC_EXTENDED_CHARACTER = 105,
HDR_GC_REFINE_INFORMATION_OLD = 95,
HDR_GC_OBSERVER_ADD = 96,
HDR_GC_OBSERVER_REMOVE = 97,
@@ -1459,6 +1465,9 @@ constexpr bool is_dynamic_gc(uint8_t h) {
case HDR_GC_GUILD:
case HDR_GC_MESSENGER:
case HDR_GC_QUEST_INFO:
case HDR_GC_QUEST_SCRIPT:
case HDR_GC_EXTENDED_STATUS:
case HDR_GC_EXTENDED_CHARACTER:
case HDR_GC_DUEL_START:
case HDR_GC_SYNC_POSITION:
// client CMainPacketHeaderMap registers GC_WHISPER STATIC, but its
+2
View File
@@ -496,6 +496,7 @@ void M2Client::connect_to_server(const String &auth_host, int auth_port, const S
// then connect to game_host with the returned login ticket.
if (OS::get_singleton()->get_environment("MT_PROTOCOL") == "classic") {
classic_sess = std::make_unique<mtnet::classic::ClassicSession>();
classic_sess->set_wire_trace(OS::get_singleton()->get_environment("MT_NET_TRACE") == "1");
// §1.5 (W1 G1 wiring): CG_CLIENT_VERSION2 reports the running binary's
// filename; feed it the real executable name rather than the built-in
// default so the server-side version gate sees what ClientVS22 sends.
@@ -2875,6 +2876,7 @@ void M2Client::warp_to_game_server(const String &host, int port) {
mall_open_seen = false;
dead_seen.clear();
classic_sess = std::make_unique<mtnet::classic::ClassicSession>();
classic_sess->set_wire_trace(OS::get_singleton()->get_environment("MT_NET_TRACE") == "1");
bool connected = false;
if (last_login_key != 0) {
connected = classic_sess->connect_with_login_key(std::string(host.utf8().get_data()),