#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 #include #include #include #include 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 &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 &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 &)> on_char_list; std::function on_entered_game; std::function on_stage_change; std::function 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 &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