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:
co-authored by
Claude Sonnet 5
parent
f32064c74a
commit
b296457e3c
@@ -1,10 +1,31 @@
|
||||
#include "classic_parser.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace mtnet::classic {
|
||||
|
||||
void ClassicParser::set_npclist_path(std::string path) {
|
||||
if (path == m_npclist_path) {
|
||||
return;
|
||||
}
|
||||
m_npclist_path = std::move(path);
|
||||
m_npc_names.clear();
|
||||
m_npc_names_loaded = false;
|
||||
}
|
||||
|
||||
const std::string &ClassicParser::npc_name_for(uint16_t race) {
|
||||
static const std::string kEmpty;
|
||||
if (!m_npc_names_loaded) {
|
||||
m_npc_names = load_npclist(m_npclist_path.empty() ? default_npclist_path() : m_npclist_path);
|
||||
m_npc_names_loaded = true;
|
||||
}
|
||||
const auto it = m_npc_names.find(race);
|
||||
return it == m_npc_names.end() ? kEmpty : it->second;
|
||||
}
|
||||
|
||||
void ClassicParser::parse_login_success(const SimplePlayer *players, const uint32_t *guild_id,
|
||||
const char (*guild_name)[GUILD_NAME_MAX_LEN + 1], int n, uint32_t handle, uint32_t rkey) {
|
||||
m_slots.clear();
|
||||
@@ -207,7 +228,36 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
|
||||
if (!fill(p, header, body, len)) {
|
||||
return false;
|
||||
}
|
||||
m_world.mut_spawn(p.vid, p.race, p.type, /*name*/ std::string(), (float)p.x, (float)p.y,
|
||||
// §0.3 / §2.2: the reference client (IsInvisibleRace) never spawns
|
||||
// these — drop before any stash / spawn.
|
||||
if (is_invisible_race(p.race)) {
|
||||
return true;
|
||||
}
|
||||
if (p.type == CHRTYPE_PC || p.type == CHRTYPE_NPC) {
|
||||
// Two-packet actor: GC_CHARACTER_ADD carries no name / guild /
|
||||
// parts, so stash a bare record and wait for
|
||||
// GC_CHAR_ADDITIONAL_INFO to merge + flush it. The world model
|
||||
// is untouched until then (§0.2 data layer only).
|
||||
Entity &e = m_pending_actor[p.vid];
|
||||
e = Entity{};
|
||||
e.vid = p.vid;
|
||||
e.race = p.race;
|
||||
e.ch_type = p.type;
|
||||
e.x = (float)p.x;
|
||||
e.y = (float)p.y;
|
||||
e.z = (float)p.z;
|
||||
e.angle = p.angle;
|
||||
e.moving_speed = p.moving_speed;
|
||||
e.attack_speed = p.attack_speed;
|
||||
e.state_flags = p.state_flag;
|
||||
e.affect_flags = (uint64_t)p.affect_flag[0] |
|
||||
((uint64_t)p.affect_flag[1] << 32);
|
||||
return true;
|
||||
}
|
||||
// Monster / stone / door / object: appended immediately, name from
|
||||
// npclist.txt (race -> code, like CPythonNonPlayer::GetName); every
|
||||
// PC-specific field stays 0.
|
||||
m_world.mut_spawn(p.vid, p.race, p.type, npc_name_for(p.race), (float)p.x, (float)p.y,
|
||||
(float)p.z, p.angle, p.moving_speed, p.attack_speed);
|
||||
return true;
|
||||
}
|
||||
@@ -396,9 +446,31 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
|
||||
if (!fill(p, header, body, len)) {
|
||||
return false;
|
||||
}
|
||||
m_world.mut_char_info(p.vid, std::string(p.name, strnlen(p.name, sizeof(p.name))),
|
||||
p.parts, p.empire, (int32_t)p.guild_id, (int32_t)p.level, p.alignment,
|
||||
p.pk_mode, p.mount_vnum);
|
||||
// §2.2: this only ever completes a pending GC_CHARACTER_ADD (the
|
||||
// reference merges into s_kNetActorData, populated by that add). No
|
||||
// pending record -> nothing to build a PC/NPC from: record + drop,
|
||||
// never half-create.
|
||||
const auto it = m_pending_actor.find(p.vid);
|
||||
if (it == m_pending_actor.end()) {
|
||||
m_last_error = "GC_CHAR_ADDITIONAL_INFO: no pending actor for vid " +
|
||||
std::to_string(p.vid);
|
||||
m_unhandled_headers.push_back(header);
|
||||
return true;
|
||||
}
|
||||
Entity e = it->second;
|
||||
e.vid = p.vid;
|
||||
e.name = mtnet::from_wire_str(p.name, sizeof(p.name)); // §1.10
|
||||
e.guild = (int32_t)p.guild_id;
|
||||
e.level = (int32_t)p.level;
|
||||
e.alignment = p.alignment;
|
||||
e.pk_mode = p.pk_mode;
|
||||
e.empire = p.empire;
|
||||
for (int i = 0; i < CHR_EQUIPPART_NUM; ++i) {
|
||||
e.parts[i] = p.parts[i];
|
||||
}
|
||||
e.mount_vnum = p.mount_vnum;
|
||||
m_world.mut_spawn_full(e);
|
||||
m_pending_actor.erase(it);
|
||||
return true;
|
||||
}
|
||||
case HDR_GC_SKILL_LEVEL: { // 76
|
||||
|
||||
Reference in New Issue
Block a user