40250 classic: increment 50 (Phase 1 W1) — §1.5 client version, §1.10 text codec, §2.2 two-packet merge

§1.5 CG_CLIENT_VERSION: send_client_version() now branches on locale. EUROPE
(this project's locale) sends CGClientVersion2 (header 0xf1, 67B, timestamp
"1215955205"); non-EUROPE sends CGClientVersion (0xfd) + CMake-injected
MT_BUILD_TIMESTAMP in C __TIMESTAMP__ form. New ClassicSession::set_executable_name()
(default "metin2.bin"; TODO(W0): M2Client passes OS::get_executable_path().get_file()).
m_version_sent set only after send_fixed() succeeds. wire_classic.h gains
CGClientVersion2 + size-table entry.

§1.10 fixed-length text: new extension/src/net/text_codec.h — to_wire()/
from_wire_str() apply a hard byte cap without splitting a UTF-8 multibyte
sequence; godot::String overloads are __has_include-guarded so mtnet stays
godot-free. classic_session.cpp routes 8 fixed-length name/sign/comment fills
through to_wire(); parser reads GC_CHAR_ADDITIONAL_INFO name via from_wire_str().
New net_text_codec_test (ctest net.text_codec). Locale codepage (CP949/CP1252)
transcoding remains a follow-up; wire bytes are currently UTF-8 (ASCII round-trips).

§2.2 two-packet PC/NPC merge: ClassicParser GC_CHARACTER_ADD drops invisible
races (20025/20038/20039); PC/NPC only stash a bare Entity into m_pending_actor;
everything else spawns immediately with a name from root/npclist.txt (new
npc_names.h + set_npclist_path(); $MT_ASSETS/$M2_ASSETS fallback; TODO(W0):
M2Client passes the asset root). GC_CHAR_ADDITIONAL_INFO with no pending entry
records m_last_error + m_unhandled_headers and drops (never half-creates);
on a hit it merges the pending Entity with the additional-info's 9 fields into
one mut_spawn_full() then erases the pending entry. New pending_actor_count()
test hook; net_classic_session_test updated (version assertions -> 0xf1;
stash -> merge -> spawn, immediate monster spawn, invisible-race drop,
no-pending additional-info is a no-op). Depends on W2 §2.2 steps 4-5
(entity_store mut_char_info touch / mut_shop_sign cleanup) — the merge works
with current W2 code as-is.

Build + ctest 17/17 (16 + net.text_codec); netbridge_test, p10_test green.
Docs: CLIENT-GAP.md W1 row + increment-50 change-log entry; CLIENT-GAP-FIX.md
§1.5/§1.10/§2.2 status + facts and C.5 batch record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
This commit is contained in:
shenlei
2026-09-02 15:51:48 +09:00
co-authored by Claude Sonnet 5
parent f32064c74a
commit b296457e3c
12 changed files with 742 additions and 74 deletions
@@ -17,6 +17,7 @@
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
namespace mtnet::classic {
@@ -159,6 +160,18 @@ public:
// caller drives enter_game()). The 40250 test account sends a large map-load
// burst; waiting for that burst to drain avoids racing the server's spawn data.
void set_auto_entergame_delay(uint32_t ms) { m_entergame_delay = ms; }
// §1.5: the client-version report (CG_CLIENT_VERSION / _VERSION2) carries the
// running executable's file name — the stock client passes the launched
// .exe/.bin name (CPythonApplication). The net layer has no way to know it,
// so it stays "metin2.bin" until a host sets it.
// TODO(W0): M2Client should call
// classic_sess->set_executable_name(OS::get_executable_path().get_file())
void set_executable_name(std::string name) { m_executable_name = std::move(name); }
// §1.5: EUROPE-family locales send CG_CLIENT_VERSION2 (0xf1) + the fixed ymir
// epoch string "1215955205"; every other locale sends CG_CLIENT_VERSION
// (0xfd) + the build timestamp. This project's LOCALE_SERVICE_* resolves to
// EUROPE (see docs/CLIENT-GAP-FIX.md §1.5), hence the default.
void set_locale_is_europe(bool on) { m_locale_is_europe = on; }
void set_wire_trace(bool on) {
m_stream.set_wire_trace(on);
m_auth_stream.set_wire_trace(on);
@@ -200,6 +213,8 @@ private:
bool m_authenticated_game = false;
bool m_auth_transition = false;
bool m_version_sent = false;
bool m_locale_is_europe = true;
std::string m_executable_name = "metin2.bin";
bool m_game_connection_seen = false;
bool m_direct_enter = false;
int m_direct_enter_slot = -1;