§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
225 lines
9.8 KiB
C++
225 lines
9.8 KiB
C++
#pragma once
|
|
// ClassicSession — MT_PROTOCOL=classic: drives ClassicStream through the
|
|
// 40250 auth-server -> game-server flow and parses GC packets into a shared
|
|
// EntityStore. See docs/CLIENT-40250-PORT.md §4 / §8.
|
|
//
|
|
// The stock 40250 server uses _IMPROVED_PACKET_ENCRYPTION_; ClassicStream
|
|
// negotiates that DH2 + CTR session before the first login packet.
|
|
|
|
#include "classic_parser.h"
|
|
#include "classic_stream.h"
|
|
#include "wire_classic.h"
|
|
|
|
#include "../entity_store.h"
|
|
#include "../i_net_session.h"
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
namespace mtnet::classic {
|
|
|
|
class ClassicSession : public mtnet::INetSession {
|
|
public:
|
|
ClassicSession();
|
|
|
|
// --- INetSession ---
|
|
bool connect(const std::string &game_host, uint16_t game_port, const std::string &id,
|
|
const std::string &pw) override;
|
|
// Full 40250 flow: auth CG_LOGIN3 -> ticket, then game CG_LOGIN2 -> chars.
|
|
bool connect(const std::string &auth_host, uint16_t auth_port,
|
|
const std::string &game_host, uint16_t game_port, const std::string &id,
|
|
const std::string &pw);
|
|
// Reconnect directly to a game channel using a previously issued auth ticket.
|
|
bool connect_with_login_key(const std::string &game_host, uint16_t game_port,
|
|
const std::string &id, uint32_t login_key);
|
|
void disconnect() override;
|
|
bool select_char(int slot) override;
|
|
bool enter_game() override;
|
|
// ClientVS22 DirectEnter: reconnect to the slot's lAddr:wPort, then the
|
|
// loading phase sends CG_CHARACTER_SELECT on the new game connection.
|
|
bool connect_direct_enter(int slot);
|
|
// ClientVS22 GC_WARP: every warp is a new game connection to the address
|
|
// carried by the packet. The selected slot and login ticket are retained.
|
|
bool connect_warp(const std::string &game_host, uint16_t game_port);
|
|
bool create_character(int slot, const std::string &name, int job, int shape,
|
|
int con, int intel, int str, int dex);
|
|
bool delete_character(int slot, const std::string &private_code);
|
|
bool change_name(int slot, const std::string &name);
|
|
bool send_empire(uint8_t empire);
|
|
void pump() override;
|
|
Stage stage() const override { return m_stage; }
|
|
const std::string &last_error() const override { return m_last_error; }
|
|
EntityStore &world() override { return m_world; }
|
|
|
|
bool send_move(uint8_t func, uint8_t arg, float rot_deg, int32_t x, int32_t y,
|
|
uint32_t time_ms = 0) override;
|
|
bool send_attack(uint8_t type, uint32_t victim_vid) override;
|
|
bool send_chat(uint8_t type, const std::string &text) override;
|
|
bool send_target(uint32_t vid) override;
|
|
bool send_character_position(uint8_t position) override;
|
|
bool send_click_npc(uint32_t vid) override;
|
|
bool send_script_answer(uint8_t answer) override;
|
|
bool send_item_use(uint8_t window, uint16_t cell) override;
|
|
bool send_item_move(uint8_t window, uint16_t cell, uint8_t to_window, uint16_t to_cell,
|
|
uint8_t count) override;
|
|
bool send_item_drop(uint8_t window, uint16_t cell, uint32_t gold, uint8_t count) override;
|
|
bool send_item_pickup(uint32_t ground_vid) override;
|
|
bool send_use_skill(uint32_t skill_vnum, uint32_t target_vid) override;
|
|
bool send_item_use_to_item(uint8_t window, uint16_t cell, uint8_t target_window,
|
|
uint16_t target_cell);
|
|
bool send_item_give(uint32_t target_vid, uint8_t window, uint16_t cell, uint8_t count);
|
|
bool send_request_warp();
|
|
bool send_fishing(uint8_t rot);
|
|
bool send_dungeon();
|
|
bool send_sync_positions(const std::vector<SyncPosElement> &positions);
|
|
bool send_whisper(const std::string &to, const std::string &text);
|
|
bool send_shoot(uint8_t type);
|
|
bool send_add_fly_targeting(uint32_t target_vid, int32_t x, int32_t y);
|
|
bool send_quickslot_add(uint8_t pos, uint8_t type, uint8_t ref);
|
|
bool send_quickslot_del(uint8_t pos);
|
|
bool send_quickslot_swap(uint8_t pos, uint8_t pos_to);
|
|
bool send_script_button(uint32_t index);
|
|
bool send_script_select_item(uint32_t selection);
|
|
bool send_quest_input(const std::string &text);
|
|
bool send_quest_confirm(bool yes, uint32_t request_pid);
|
|
bool send_quest_cancel();
|
|
bool send_party_invite(uint32_t vid);
|
|
bool send_party_answer(uint32_t leader_vid, bool accept);
|
|
bool send_party_remove(uint32_t pid);
|
|
bool send_party_set_state(uint32_t pid, uint8_t role, bool on);
|
|
bool send_party_use_skill(uint8_t skill_index, uint32_t vid);
|
|
bool send_party_parameter(uint8_t mode);
|
|
bool send_shop_buy(uint8_t pos, uint8_t count);
|
|
bool send_shop_sell(uint8_t pos, uint8_t count);
|
|
bool send_shop_close();
|
|
bool send_private_shop(const std::string &sign, const std::vector<CGMyShopItem> &items);
|
|
bool send_cube_make(int result_index);
|
|
bool send_cube_result_list(int npc_vnum);
|
|
bool send_cube_materials(int start_index, int count);
|
|
bool send_cube_open();
|
|
bool send_cube_close();
|
|
bool send_cube_list();
|
|
bool send_cube_add_item(int cube_index, int inventory_index);
|
|
bool send_cube_delete_item(int cube_index);
|
|
bool send_guild_sub(uint8_t subheader);
|
|
bool send_guild_u32(uint8_t subheader, uint32_t value);
|
|
bool send_guild_i32(uint8_t subheader, int32_t value);
|
|
bool send_guild_grade_name(uint8_t grade, const std::string &name);
|
|
bool send_guild_grade_authority(uint8_t grade, uint8_t authority);
|
|
bool send_guild_member_grade(uint32_t pid, uint8_t grade);
|
|
bool send_guild_member_general(uint32_t pid, bool enabled);
|
|
bool send_guild_comment(const std::string &text);
|
|
bool send_guild_invite_answer(uint32_t guild_id, bool accept);
|
|
bool send_guild_answer_make(const std::string &name);
|
|
bool send_guild_skill(uint32_t skill_vnum, uint32_t target_pid);
|
|
bool send_refine(uint8_t pos, uint8_t type);
|
|
bool send_dragon_soul_refine(uint8_t subheader, const ItemPos *grid, size_t count);
|
|
bool send_friend_add(const std::string &name) override;
|
|
bool send_friend_remove(const std::string &name) override;
|
|
bool send_exchange_start(uint32_t partner_vid) override;
|
|
bool send_exchange_item_add(uint8_t inv_window, uint16_t inv_cell,
|
|
uint8_t display_pos) override;
|
|
bool send_exchange_gold(uint32_t gold) override;
|
|
bool send_exchange_accept() override;
|
|
bool send_exchange_cancel() override;
|
|
bool send_safebox_checkin(uint8_t safe_pos, uint8_t inv_window,
|
|
uint16_t inv_cell) override;
|
|
bool send_safebox_checkout(uint8_t safe_pos, uint8_t inv_window,
|
|
uint16_t inv_cell) override;
|
|
bool send_safebox_move(uint16_t from_cell, uint16_t to_cell, uint8_t count) override;
|
|
bool send_mall_checkout(uint8_t mall_pos, uint8_t inv_window,
|
|
uint16_t inv_cell) override;
|
|
|
|
// --- callbacks ---
|
|
std::function<void(const std::vector<CharSlot> &)> on_char_list;
|
|
std::function<void()> on_entered_game;
|
|
std::function<void(Stage)> on_stage_change;
|
|
std::function<void(const std::string &)> on_error;
|
|
|
|
// Set up creds + state WITHOUT opening a socket — feed the stream by hand via
|
|
// stream().feed()/take_outgoing(). Used by the offline flow test and any
|
|
// non-TCP transport. connect() == start_offline() + m_stream.connect().
|
|
void start_offline(const std::string &id, const std::string &pw);
|
|
|
|
// --- accessors ---
|
|
const std::vector<CharSlot> &char_slots() const { return m_parser.char_slots(); }
|
|
ClassicStream &stream() { return m_stream; } // for tests / trace
|
|
ClassicStream &auth_stream() { return m_auth_stream; } // auth-flow tests / trace
|
|
ClassicParser &parser() { return m_parser; }
|
|
uint32_t login_key() const { return m_auth_login_key; }
|
|
bool was_online_lost() const { return m_game_connection_seen; }
|
|
uint32_t now_ms() const { return m_now; }
|
|
void set_now(uint32_t ms) {
|
|
m_now = ms;
|
|
m_world.set_now(ms);
|
|
}
|
|
// auto-send CG_ENTERGAME this many ms after entering PHASE_LOADING (0 = never,
|
|
// 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);
|
|
}
|
|
|
|
private:
|
|
void set_stage(Stage s);
|
|
void on_phase(uint8_t phase);
|
|
void on_auth_phase(uint8_t phase);
|
|
bool on_packet(uint8_t header, const uint8_t *body, uint32_t len);
|
|
bool on_auth_packet(uint8_t header, const uint8_t *body, uint32_t len);
|
|
bool send_login();
|
|
bool send_auth_login();
|
|
bool send_login_by_key();
|
|
bool send_client_version();
|
|
bool send_select_char(int slot);
|
|
|
|
EntityStore m_world;
|
|
ClassicStream m_stream;
|
|
ClassicStream m_auth_stream;
|
|
ClassicParser m_parser;
|
|
|
|
Stage m_stage = Stage::Offline;
|
|
std::string m_last_error;
|
|
std::string m_id, m_pw;
|
|
std::string m_auth_host, m_game_host;
|
|
uint16_t m_auth_port = 0;
|
|
uint16_t m_game_port = 0;
|
|
uint32_t m_auth_login_key = 0;
|
|
uint32_t m_now = 0;
|
|
uint32_t m_loading_since = 0;
|
|
uint32_t m_entergame_delay = 8000;
|
|
bool m_entergame_sent = false;
|
|
bool m_login_sent = false;
|
|
bool m_auth_login_sent = false;
|
|
bool m_use_auth = false;
|
|
bool m_auth_succeeded = false;
|
|
bool m_game_started = false;
|
|
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;
|
|
int m_selected_slot = -1;
|
|
};
|
|
|
|
} // namespace mtnet::classic
|