客户端问题修改
This commit is contained in:
@@ -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>();
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user