#include "m2_client.h" #include "auth_client.h" #include "classic/classic_mark_client.h" #include "classic/classic_session.h" #include "game_client.h" #include "mark_client.h" #include "net_bounds.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace godot; namespace mtgodot { namespace { bool valid_angle(double degrees) { return std::isfinite(degrees); } bool valid_item_pos(int window, int cell) { return mtnet::bounds::u8(window) && mtnet::bounds::u16(cell); } bool read_int(const Dictionary &dict, const char *key, int64_t &value, int64_t fallback) { const Variant raw = dict.get(key, fallback); if (raw.get_type() != Variant::INT) { return false; } value = static_cast(raw); return true; } } // namespace M2Client::M2Client() { set_process(true); } M2Client::~M2Client() = default; void M2Client::_bind_methods() { ClassDB::bind_method( D_METHOD("connect_to_server", "auth_host", "auth_port", "game_host", "game_port", "id", "pw"), &M2Client::connect_to_server); ClassDB::bind_method(D_METHOD("disconnect_from_server"), &M2Client::disconnect_from_server); ClassDB::bind_method(D_METHOD("select_character", "index"), &M2Client::select_character); ClassDB::bind_method(D_METHOD("enter_game", "index"), &M2Client::enter_game); ClassDB::bind_method( D_METHOD("create_character", "slot", "name", "job", "shape", "con", "int", "str", "dex"), &M2Client::create_character); ClassDB::bind_method(D_METHOD("delete_character", "slot", "private_code"), &M2Client::delete_character); ClassDB::bind_method(D_METHOD("change_name", "slot", "name"), &M2Client::change_name); ClassDB::bind_method(D_METHOD("get_empire"), &M2Client::get_empire); ClassDB::bind_method(D_METHOD("select_empire", "empire"), &M2Client::select_empire); ClassDB::bind_method(D_METHOD("get_slot_count"), &M2Client::get_slot_count); ClassDB::bind_method(D_METHOD("move", "func", "arg", "rot_deg", "x", "y"), &M2Client::move); ClassDB::bind_method(D_METHOD("character_position", "position"), &M2Client::character_position); ClassDB::bind_method(D_METHOD("sync_positions", "positions"), &M2Client::sync_positions); ClassDB::bind_method(D_METHOD("request_warp"), &M2Client::request_warp); ClassDB::bind_method(D_METHOD("fishing", "rot_deg"), &M2Client::fishing); ClassDB::bind_method(D_METHOD("request_dungeon"), &M2Client::request_dungeon); ClassDB::bind_method(D_METHOD("attack", "motion", "victim_vid"), &M2Client::attack); ClassDB::bind_method(D_METHOD("set_target", "victim_vid"), &M2Client::set_target); ClassDB::bind_method(D_METHOD("say", "type", "text"), &M2Client::say); ClassDB::bind_method(D_METHOD("send_emoticon", "index"), &M2Client::send_emoticon); ClassDB::bind_method(D_METHOD("whisper", "to", "text"), &M2Client::whisper); ClassDB::bind_method(D_METHOD("cast_skill", "motion_idx", "rot_deg", "x", "y"), &M2Client::cast_skill); ClassDB::bind_method(D_METHOD("use_skill", "skill_id", "target_vid"), &M2Client::use_skill); ClassDB::bind_method(D_METHOD("shoot", "skill_id"), &M2Client::shoot); ClassDB::bind_method(D_METHOD("add_fly_targeting", "target_vid", "x", "y"), &M2Client::add_fly_targeting); ClassDB::bind_method(D_METHOD("skill_up", "skill_id"), &M2Client::skill_up); ClassDB::bind_method(D_METHOD("get_skill_group"), &M2Client::get_skill_group); ClassDB::bind_method(D_METHOD("get_skills"), &M2Client::get_skills); ClassDB::bind_method(D_METHOD("get_quickslots"), &M2Client::get_quickslots); ClassDB::bind_method(D_METHOD("quickslot_add", "pos", "type", "ref"), &M2Client::quickslot_add); ClassDB::bind_method(D_METHOD("quickslot_del", "pos"), &M2Client::quickslot_del); ClassDB::bind_method(D_METHOD("quickslot_swap", "pos", "change_pos"), &M2Client::quickslot_swap); ClassDB::bind_method(D_METHOD("click_npc", "vid"), &M2Client::click_npc); ClassDB::bind_method(D_METHOD("script_answer", "answer"), &M2Client::script_answer); ClassDB::bind_method(D_METHOD("script_button", "idx"), &M2Client::script_button); ClassDB::bind_method(D_METHOD("script_select_item", "selection"), &M2Client::script_select_item); ClassDB::bind_method(D_METHOD("quest_input", "text"), &M2Client::quest_input); ClassDB::bind_method(D_METHOD("quest_confirm", "yes", "request_pid"), &M2Client::quest_confirm); ClassDB::bind_method(D_METHOD("quest_cancel"), &M2Client::quest_cancel); ClassDB::bind_method(D_METHOD("get_quests"), &M2Client::get_quests); ClassDB::bind_method(D_METHOD("get_entity", "vid"), &M2Client::get_entity); ClassDB::bind_method(D_METHOD("get_entities"), &M2Client::get_entities); ClassDB::bind_method(D_METHOD("get_main_vid"), &M2Client::get_main_vid); ClassDB::bind_method(D_METHOD("get_main_pid"), &M2Client::get_main_pid); ClassDB::bind_method(D_METHOD("get_points"), &M2Client::get_points); ClassDB::bind_method(D_METHOD("get_target"), &M2Client::get_target); ClassDB::bind_method(D_METHOD("get_affects"), &M2Client::get_affects); ClassDB::bind_method(D_METHOD("move_item", "from_window", "from_cell", "to_window", "to_cell", "count"), &M2Client::move_item); ClassDB::bind_method(D_METHOD("use_item", "window", "cell"), &M2Client::use_item); ClassDB::bind_method(D_METHOD("drop_item", "window", "cell", "gold"), &M2Client::drop_item); ClassDB::bind_method(D_METHOD("drop_item_count", "window", "cell", "gold", "count"), &M2Client::drop_item_count); ClassDB::bind_method(D_METHOD("use_item_to_item", "source_window", "source_cell", "target_window", "target_cell"), &M2Client::use_item_to_item); ClassDB::bind_method(D_METHOD("give_item", "target_vid", "window", "cell", "count"), &M2Client::give_item); ClassDB::bind_method(D_METHOD("pickup_item", "ground_vid"), &M2Client::pickup_item); ClassDB::bind_method(D_METHOD("get_inventory"), &M2Client::get_inventory); ClassDB::bind_method(D_METHOD("get_equipment"), &M2Client::get_equipment); ClassDB::bind_method(D_METHOD("get_belt_inventory"), &M2Client::get_belt_inventory); ClassDB::bind_method(D_METHOD("get_view_equipment", "vid"), &M2Client::get_view_equipment); ClassDB::bind_method(D_METHOD("get_item", "window", "cell"), &M2Client::get_item); ClassDB::bind_method(D_METHOD("get_ground_items"), &M2Client::get_ground_items); ClassDB::bind_method(D_METHOD("get_pvp_relations"), &M2Client::get_pvp_relations); ClassDB::bind_method(D_METHOD("get_duel"), &M2Client::get_duel); ClassDB::bind_method(D_METHOD("suspend"), &M2Client::suspend); ClassDB::bind_method(D_METHOD("resume"), &M2Client::resume); ClassDB::bind_method(D_METHOD("reconnect"), &M2Client::reconnect); ClassDB::bind_method(D_METHOD("is_suspended"), &M2Client::is_suspended); ClassDB::bind_method(D_METHOD("get_stage"), &M2Client::get_stage); ClassDB::bind_method(D_METHOD("is_in_game"), &M2Client::is_in_game); ClassDB::bind_method(D_METHOD("net_poll"), &M2Client::net_poll); ADD_SIGNAL(MethodInfo("stage_changed", PropertyInfo(Variant::STRING, "stage"))); ADD_SIGNAL(MethodInfo("phase_changed", PropertyInfo(Variant::STRING, "phase"))); ADD_SIGNAL(MethodInfo("auth_ok", PropertyInfo(Variant::INT, "login_key"))); ADD_SIGNAL(MethodInfo("login_failed", PropertyInfo(Variant::STRING, "reason"))); ADD_SIGNAL(MethodInfo("char_list", PropertyInfo(Variant::ARRAY, "characters"))); ADD_SIGNAL(MethodInfo("char_created", PropertyInfo(Variant::INT, "slot"))); ADD_SIGNAL(MethodInfo("char_create_failed", PropertyInfo(Variant::INT, "reason_type"))); ADD_SIGNAL(MethodInfo("char_deleted", PropertyInfo(Variant::INT, "slot"))); ADD_SIGNAL(MethodInfo("char_delete_failed")); ADD_SIGNAL(MethodInfo("empire_changed", PropertyInfo(Variant::INT, "empire"))); ADD_SIGNAL(MethodInfo("char_name_changed", PropertyInfo(Variant::INT, "pid"), PropertyInfo(Variant::STRING, "name"))); ADD_SIGNAL(MethodInfo("entered_game")); ADD_SIGNAL(MethodInfo("disconnected", PropertyInfo(Variant::STRING, "reason"))); ADD_SIGNAL(MethodInfo("suspended")); ADD_SIGNAL(MethodInfo("resumed")); // networked world (fed from EntityStore each pump) ADD_SIGNAL(MethodInfo("entity_spawned", PropertyInfo(Variant::DICTIONARY, "entity"))); ADD_SIGNAL(MethodInfo("entity_despawned", PropertyInfo(Variant::INT, "vid"))); ADD_SIGNAL(MethodInfo("entity_moved", PropertyInfo(Variant::INT, "vid"))); ADD_SIGNAL(MethodInfo("entity_main_set", PropertyInfo(Variant::INT, "vid"))); // name/level/guild/mount arrived (GC_CHAR_ADD_INFO) for an already-spawned entity. ADD_SIGNAL(MethodInfo("entity_info", PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::DICTIONARY, "entity"))); ADD_SIGNAL(MethodInfo("chat", PropertyInfo(Variant::INT, "type"), PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::STRING, "text"))); ADD_SIGNAL(MethodInfo("emoticon_requested", PropertyInfo(Variant::INT, "index"))); ADD_SIGNAL(MethodInfo("whisper_received", PropertyInfo(Variant::INT, "sub"), PropertyInfo(Variant::STRING, "from"), PropertyInfo(Variant::STRING, "text"))); // combat / status ADD_SIGNAL(MethodInfo("vitals_changed", PropertyInfo(Variant::INT, "vid"))); ADD_SIGNAL(MethodInfo("entity_dead", PropertyInfo(Variant::INT, "vid"))); ADD_SIGNAL(MethodInfo("damage", PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::INT, "amount"), PropertyInfo(Variant::INT, "flag"))); ADD_SIGNAL(MethodInfo("motion", PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::INT, "victim_vid"), PropertyInfo(Variant::INT, "motion"))); ADD_SIGNAL(MethodInfo("dig_motion", PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::INT, "target_vid"), PropertyInfo(Variant::INT, "count"))); ADD_SIGNAL(MethodInfo("points_changed", PropertyInfo(Variant::DICTIONARY, "points"))); ADD_SIGNAL(MethodInfo("target_info", PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::INT, "hp_percent"))); // items ADD_SIGNAL(MethodInfo("inventory_changed", PropertyInfo(Variant::INT, "window"), PropertyInfo(Variant::INT, "cell"))); ADD_SIGNAL(MethodInfo("ground_item_added", PropertyInfo(Variant::DICTIONARY, "item"))); ADD_SIGNAL(MethodInfo("ground_item_removed", PropertyInfo(Variant::INT, "vid"))); ADD_SIGNAL(MethodInfo("view_equipment", PropertyInfo(Variant::INT, "vid"))); ADD_SIGNAL(MethodInfo("pvp_changed", PropertyInfo(Variant::INT, "src_vid"), PropertyInfo(Variant::INT, "dst_vid"), PropertyInfo(Variant::INT, "mode"))); ADD_SIGNAL(MethodInfo("duel_changed", PropertyInfo(Variant::DICTIONARY, "duel"))); ADD_SIGNAL(MethodInfo("duel_started")); ADD_SIGNAL(MethodInfo("item_picked_up", PropertyInfo(Variant::INT, "vnum"), PropertyInfo(Variant::INT, "count"), PropertyInfo(Variant::STRING, "from"))); ADD_SIGNAL(MethodInfo("item_used", PropertyInfo(Variant::INT, "vnum"))); ADD_SIGNAL(MethodInfo("affect_added", PropertyInfo(Variant::DICTIONARY, "affect"))); ADD_SIGNAL(MethodInfo("affect_removed", PropertyInfo(Variant::INT, "type"))); // P5: server-triggered effects. name is a .mse path for GC_SPECIFIC_EFFECT, // "" for GC_SPECIAL_EFFECT (use `special` id + a client-side table). ADD_SIGNAL(MethodInfo("effect_cue", PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::INT, "special"))); // P6: skills ADD_SIGNAL(MethodInfo("skills_changed")); ADD_SIGNAL(MethodInfo("skill_group_changed", PropertyInfo(Variant::INT, "skill_group"))); ADD_SIGNAL(MethodInfo("quickslots_changed")); ADD_SIGNAL(MethodInfo("skill_cooldown_end", PropertyInfo(Variant::INT, "skill"))); ADD_SIGNAL(MethodInfo("fly_cue", PropertyInfo(Variant::INT, "type"), PropertyInfo(Variant::INT, "start_vid"), PropertyInfo(Variant::INT, "end_vid"))); ADD_SIGNAL(MethodInfo("fly_targeting", PropertyInfo(Variant::INT, "shooter_vid"), PropertyInfo(Variant::INT, "target_vid"), PropertyInfo(Variant::VECTOR2, "target_cm"), PropertyInfo(Variant::BOOL, "append"))); // P7: quest / NPC ADD_SIGNAL(MethodInfo("script_dialog", PropertyInfo(Variant::INT, "skin"), PropertyInfo(Variant::STRING, "text"))); ADD_SIGNAL(MethodInfo("quest_confirm_ask", PropertyInfo(Variant::STRING, "msg"), PropertyInfo(Variant::INT, "timeout"), PropertyInfo(Variant::INT, "request_pid"))); ADD_SIGNAL(MethodInfo("quest_info", PropertyInfo(Variant::INT, "index"))); // P8: social / shop / storage ClassDB::bind_method(D_METHOD("party_invite", "vid"), &M2Client::party_invite); ClassDB::bind_method(D_METHOD("party_answer", "leader_pid", "accept"), &M2Client::party_answer); ClassDB::bind_method(D_METHOD("party_leave", "pid"), &M2Client::party_leave); ClassDB::bind_method(D_METHOD("party_use_skill", "skill_index", "target_vid"), &M2Client::party_use_skill); ClassDB::bind_method(D_METHOD("party_set_distribute", "mode"), &M2Client::party_set_distribute); ClassDB::bind_method(D_METHOD("party_set_state", "pid", "role", "on"), &M2Client::party_set_state); ClassDB::bind_method(D_METHOD("get_party"), &M2Client::get_party); ClassDB::bind_method(D_METHOD("get_party_distribute_mode"), &M2Client::get_party_distribute_mode); ClassDB::bind_method(D_METHOD("add_friend", "name"), &M2Client::add_friend); ClassDB::bind_method(D_METHOD("remove_friend", "name"), &M2Client::remove_friend); ClassDB::bind_method(D_METHOD("friend_answer", "name", "accept"), &M2Client::friend_answer); ClassDB::bind_method(D_METHOD("get_friends"), &M2Client::get_friends); ClassDB::bind_method(D_METHOD("get_lover"), &M2Client::get_lover); ClassDB::bind_method(D_METHOD("shop_buy", "pos", "count"), &M2Client::shop_buy); ClassDB::bind_method(D_METHOD("shop_sell", "inv_cell", "count"), &M2Client::shop_sell); ClassDB::bind_method(D_METHOD("shop_close"), &M2Client::shop_close); ClassDB::bind_method(D_METHOD("is_shop_open"), &M2Client::is_shop_open); ClassDB::bind_method(D_METHOD("get_shop_items"), &M2Client::get_shop_items); ClassDB::bind_method(D_METHOD("get_shop"), &M2Client::get_shop); ClassDB::bind_method(D_METHOD("exchange_start", "vid"), &M2Client::exchange_start); ClassDB::bind_method(D_METHOD("exchange_add_item", "inv_window", "inv_cell", "display_pos"), &M2Client::exchange_add_item); ClassDB::bind_method(D_METHOD("exchange_add_gold", "gold"), &M2Client::exchange_add_gold); ClassDB::bind_method(D_METHOD("exchange_accept"), &M2Client::exchange_accept); ClassDB::bind_method(D_METHOD("exchange_cancel"), &M2Client::exchange_cancel); ClassDB::bind_method(D_METHOD("get_exchange"), &M2Client::get_exchange); ClassDB::bind_method(D_METHOD("safebox_checkin", "safe_pos", "inv_window", "inv_cell"), &M2Client::safebox_checkin); ClassDB::bind_method(D_METHOD("safebox_checkout", "safe_pos", "inv_window", "inv_cell"), &M2Client::safebox_checkout); ClassDB::bind_method(D_METHOD("safebox_move", "from_cell", "to_cell", "count"), &M2Client::safebox_move); ClassDB::bind_method(D_METHOD("is_safebox_open"), &M2Client::is_safebox_open); ClassDB::bind_method(D_METHOD("get_safebox_size"), &M2Client::get_safebox_size); ClassDB::bind_method(D_METHOD("get_safebox_gold"), &M2Client::get_safebox_gold); ClassDB::bind_method(D_METHOD("get_safebox_items"), &M2Client::get_safebox_items); ClassDB::bind_method(D_METHOD("safebox_password", "password"), &M2Client::safebox_password); ClassDB::bind_method(D_METHOD("safebox_change_password", "old_password", "new_password"), &M2Client::safebox_change_password); ClassDB::bind_method(D_METHOD("safebox_close"), &M2Client::safebox_close); // item-mall (창고몰) ClassDB::bind_method(D_METHOD("is_mall_open"), &M2Client::is_mall_open); ClassDB::bind_method(D_METHOD("get_mall_size"), &M2Client::get_mall_size); ClassDB::bind_method(D_METHOD("get_mall_items"), &M2Client::get_mall_items); ClassDB::bind_method(D_METHOD("mall_checkout", "mall_pos", "inv_window", "inv_cell"), &M2Client::mall_checkout); ClassDB::bind_method(D_METHOD("mall_password", "password"), &M2Client::mall_password); ClassDB::bind_method(D_METHOD("mall_close"), &M2Client::mall_close); // private (PC) shop ClassDB::bind_method(D_METHOD("open_private_shop", "sign", "items"), &M2Client::open_private_shop); ClassDB::bind_method(D_METHOD("close_private_shop"), &M2Client::close_private_shop); // cube (제작) ClassDB::bind_method(D_METHOD("get_cube"), &M2Client::get_cube); ClassDB::bind_method(D_METHOD("cube_make", "result_index"), &M2Client::cube_make); ClassDB::bind_method(D_METHOD("cube_request_result_list", "npc_vnum"), &M2Client::cube_request_result_list); ClassDB::bind_method(D_METHOD("cube_request_materials", "start_index", "count"), &M2Client::cube_request_materials); ClassDB::bind_method(D_METHOD("cube_open"), &M2Client::cube_open); ClassDB::bind_method(D_METHOD("cube_close"), &M2Client::cube_close); ClassDB::bind_method(D_METHOD("cube_list"), &M2Client::cube_list); ClassDB::bind_method(D_METHOD("cube_add_item", "cube_index", "inventory_index"), &M2Client::cube_add_item); ClassDB::bind_method(D_METHOD("cube_delete_item", "cube_index"), &M2Client::cube_delete_item); ADD_SIGNAL(MethodInfo("party_changed")); ADD_SIGNAL(MethodInfo("party_invite_ask", PropertyInfo(Variant::INT, "leader_pid"))); ADD_SIGNAL(MethodInfo("party_request_denied")); ADD_SIGNAL(MethodInfo("lover_changed", PropertyInfo(Variant::DICTIONARY, "lover"))); ADD_SIGNAL(MethodInfo("friends_changed")); ADD_SIGNAL(MethodInfo("friend_invite_ask", PropertyInfo(Variant::STRING, "name"))); ADD_SIGNAL(MethodInfo("shop_opened", PropertyInfo(Variant::INT, "vid"))); ADD_SIGNAL(MethodInfo("shop_closed")); ADD_SIGNAL(MethodInfo("shop_error", PropertyInfo(Variant::STRING, "kind"))); ADD_SIGNAL(MethodInfo("exchange_changed")); ADD_SIGNAL(MethodInfo("safebox_changed")); ADD_SIGNAL(MethodInfo("safebox_password_required")); ADD_SIGNAL(MethodInfo("safebox_wrong_password")); ADD_SIGNAL(MethodInfo("mall_opened", PropertyInfo(Variant::INT, "size"))); ADD_SIGNAL(MethodInfo("mall_changed")); ADD_SIGNAL(MethodInfo("mall_password_required")); ADD_SIGNAL(MethodInfo("private_shop_open_requested")); ADD_SIGNAL(MethodInfo("my_shop_price_list", PropertyInfo(Variant::INT, "vnum"), PropertyInfo(Variant::INT, "price"))); ADD_SIGNAL(MethodInfo("block_mode_changed", PropertyInfo(Variant::INT, "mask"))); ADD_SIGNAL(MethodInfo("observer_mode_changed", PropertyInfo(Variant::BOOL, "enabled"))); ADD_SIGNAL(MethodInfo("observer_count_changed", PropertyInfo(Variant::INT, "count"))); ADD_SIGNAL(MethodInfo("stone_detected", PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::INT, "distance"), PropertyInfo(Variant::FLOAT, "angle"))); ADD_SIGNAL(MethodInfo("stamina_changed", PropertyInfo(Variant::INT, "current"), PropertyInfo(Variant::INT, "per_sec"), PropertyInfo(Variant::BOOL, "consuming"))); ADD_SIGNAL(MethodInfo("mobile_flag_changed", PropertyInfo(Variant::BOOL, "enabled"))); ADD_SIGNAL(MethodInfo("mobile_auth_required")); ADD_SIGNAL(MethodInfo("combo_changed", PropertyInfo(Variant::BOOL, "enabled"))); ADD_SIGNAL(MethodInfo("gift_available")); ADD_SIGNAL(MethodInfo("cube_opened", PropertyInfo(Variant::INT, "npc_vnum"))); ADD_SIGNAL(MethodInfo("cube_closed")); ADD_SIGNAL(MethodInfo("cube_changed")); ADD_SIGNAL(MethodInfo("cube_result", PropertyInfo(Variant::INT, "vnum"), PropertyInfo(Variant::INT, "count"), PropertyInfo(Variant::BOOL, "ok"))); // guild / refine ClassDB::bind_method(D_METHOD("get_guild"), &M2Client::get_guild); ClassDB::bind_method(D_METHOD("get_guild_members"), &M2Client::get_guild_members); ClassDB::bind_method(D_METHOD("get_guild_grades"), &M2Client::get_guild_grades); ClassDB::bind_method(D_METHOD("guild_add_member", "vid"), &M2Client::guild_add_member); ClassDB::bind_method(D_METHOD("guild_remove_member", "pid"), &M2Client::guild_remove_member); ClassDB::bind_method(D_METHOD("guild_offer", "amount"), &M2Client::guild_offer); ClassDB::bind_method(D_METHOD("guild_charge_gsp", "amount"), &M2Client::guild_charge_gsp); ClassDB::bind_method(D_METHOD("guild_deposit_money", "amount"), &M2Client::guild_deposit_money); ClassDB::bind_method(D_METHOD("guild_withdraw_money", "amount"), &M2Client::guild_withdraw_money); ClassDB::bind_method(D_METHOD("guild_change_grade_name", "grade", "name"), &M2Client::guild_change_grade_name); ClassDB::bind_method(D_METHOD("guild_change_grade_authority", "grade", "authority"), &M2Client::guild_change_grade_authority); ClassDB::bind_method(D_METHOD("guild_change_member_grade", "pid", "grade"), &M2Client::guild_change_member_grade); ClassDB::bind_method(D_METHOD("guild_change_member_general", "pid", "enabled"), &M2Client::guild_change_member_general); ClassDB::bind_method(D_METHOD("guild_post_comment", "text"), &M2Client::guild_post_comment); ClassDB::bind_method(D_METHOD("guild_delete_comment", "comment_id"), &M2Client::guild_delete_comment); ClassDB::bind_method(D_METHOD("guild_refresh_comments"), &M2Client::guild_refresh_comments); ClassDB::bind_method(D_METHOD("guild_answer_invite", "guild_id", "accept"), &M2Client::guild_answer_invite); ClassDB::bind_method(D_METHOD("guild_answer_make", "name"), &M2Client::guild_answer_make); ClassDB::bind_method(D_METHOD("get_guild_comments"), &M2Client::get_guild_comments); ClassDB::bind_method(D_METHOD("get_guild_skill"), &M2Client::get_guild_skill); ClassDB::bind_method(D_METHOD("get_guild_wars"), &M2Client::get_guild_wars); ClassDB::bind_method(D_METHOD("get_guild_war"), &M2Client::get_guild_war); ClassDB::bind_method(D_METHOD("get_guild_name", "guild_id"), &M2Client::get_guild_name); ClassDB::bind_method(D_METHOD("use_guild_skill", "skill_vnum", "target_vid"), &M2Client::use_guild_skill); ClassDB::bind_method(D_METHOD("declare_guild_war", "guild_name"), &M2Client::declare_guild_war); ADD_SIGNAL(MethodInfo("guild_skill_changed")); ADD_SIGNAL(MethodInfo("guild_war_changed")); ADD_SIGNAL(MethodInfo("guild_comments_changed")); ADD_SIGNAL(MethodInfo("guild_invite_ask", PropertyInfo(Variant::INT, "guild_id"), PropertyInfo(Variant::STRING, "guild_name"))); ADD_SIGNAL(MethodInfo("guild_war_event", PropertyInfo(Variant::INT, "state"), PropertyInfo(Variant::INT, "opp_guild_id"), PropertyInfo(Variant::STRING, "opp_name"))); ADD_SIGNAL(MethodInfo("guild_war_point", PropertyInfo(Variant::INT, "gain_guild_id"), PropertyInfo(Variant::INT, "opp_guild_id"), PropertyInfo(Variant::INT, "point"))); ClassDB::bind_method(D_METHOD("download_guild_marks", "host", "port"), &M2Client::download_guild_marks); ClassDB::bind_method(D_METHOD("download_guild_symbol", "host", "port", "guild_id"), &M2Client::download_guild_symbol); ClassDB::bind_method(D_METHOD("get_guild_symbol"), &M2Client::get_guild_symbol); ClassDB::bind_method(D_METHOD("are_guild_marks_ready"), &M2Client::are_guild_marks_ready); ClassDB::bind_method(D_METHOD("get_mark_server"), &M2Client::get_mark_server); ClassDB::bind_method(D_METHOD("get_guild_mark", "guild_id"), &M2Client::get_guild_mark); ClassDB::bind_method(D_METHOD("get_guild_mark_image", "guild_id"), &M2Client::get_guild_mark_image); ClassDB::bind_method(D_METHOD("upload_guild_mark", "host", "port", "guild_id", "img"), &M2Client::upload_guild_mark); ClassDB::bind_method(D_METHOD("upload_guild_symbol", "host", "port", "guild_id", "file_bytes"), &M2Client::upload_guild_symbol); ADD_SIGNAL(MethodInfo("guild_marks_ready", PropertyInfo(Variant::INT, "mark_count"))); ADD_SIGNAL(MethodInfo("guild_symbol_ready", PropertyInfo(Variant::INT, "guild_id"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"))); ADD_SIGNAL(MethodInfo("guild_mark_uploaded", PropertyInfo(Variant::BOOL, "ok"))); ADD_SIGNAL(MethodInfo("guild_mark_updated", PropertyInfo(Variant::INT, "guild_id"), PropertyInfo(Variant::INT, "img_idx"))); ClassDB::bind_method(D_METHOD("refine", "pos", "type"), &M2Client::refine); ClassDB::bind_method(D_METHOD("ds_refine", "mode", "cells"), &M2Client::ds_refine); ClassDB::bind_method(D_METHOD("get_dragon_souls"), &M2Client::get_dragon_souls); ADD_SIGNAL(MethodInfo("guild_changed")); ADD_SIGNAL(MethodInfo("guild_make_requested")); ADD_SIGNAL(MethodInfo("refine_ask", PropertyInfo(Variant::DICTIONARY, "info"))); ADD_SIGNAL(MethodInfo("refine_result", PropertyInfo(Variant::BOOL, "ok"))); ADD_SIGNAL(MethodInfo("ds_window_open")); ADD_SIGNAL(MethodInfo("ds_refine_result", PropertyInfo(Variant::BOOL, "ok"), PropertyInfo(Variant::INT, "sub_type"), PropertyInfo(Variant::INT, "cell"))); // P9: world systems ClassDB::bind_method(D_METHOD("get_channel"), &M2Client::get_channel); ClassDB::bind_method(D_METHOD("get_server_time"), &M2Client::get_server_time); ClassDB::bind_method(D_METHOD("get_npc_marks"), &M2Client::get_npc_marks); ClassDB::bind_method(D_METHOD("get_land_areas"), &M2Client::get_land_areas); ClassDB::bind_method(D_METHOD("get_observers"), &M2Client::get_observers); ClassDB::bind_method(D_METHOD("get_world_markers"), &M2Client::get_world_markers); ClassDB::bind_method(D_METHOD("is_observer_mode"), &M2Client::is_observer_mode); ClassDB::bind_method(D_METHOD("get_observer_count"), &M2Client::get_observer_count); ClassDB::bind_method(D_METHOD("has_mobile_flag"), &M2Client::has_mobile_flag); ClassDB::bind_method(D_METHOD("combo_skill_enabled"), &M2Client::combo_skill_enabled); ClassDB::bind_method(D_METHOD("get_stamina_state"), &M2Client::get_stamina_state); ADD_SIGNAL(MethodInfo("warp", PropertyInfo(Variant::VECTOR3, "pos"), PropertyInfo(Variant::BOOL, "same_server"))); ADD_SIGNAL(MethodInfo("world_reset")); ADD_SIGNAL(MethodInfo("time_changed", PropertyInfo(Variant::INT, "server_epoch"))); ADD_SIGNAL(MethodInfo("channel_changed", PropertyInfo(Variant::INT, "channel"))); ADD_SIGNAL(MethodInfo("npc_marks_changed")); ADD_SIGNAL(MethodInfo("land_areas_changed")); ADD_SIGNAL(MethodInfo("observer_event", PropertyInfo(Variant::INT, "kind"), PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::VECTOR2, "pos"))); ADD_SIGNAL(MethodInfo("fishing_event", PropertyInfo(Variant::INT, "subheader"), PropertyInfo(Variant::INT, "info"), PropertyInfo(Variant::INT, "dir"))); ADD_SIGNAL(MethodInfo("dungeon_event", PropertyInfo(Variant::INT, "subheader"), PropertyInfo(Variant::INT, "x"), PropertyInfo(Variant::INT, "y"), PropertyInfo(Variant::BOOL, "has_destination"))); ADD_SIGNAL(MethodInfo("world_markers_changed")); ADD_SIGNAL(MethodInfo("mount_changed", PropertyInfo(Variant::INT, "vid"))); // §9.1 map background music: name is the resolved track id/path, volume is // 0..1 or -1 when the server left it unspecified. ADD_SIGNAL(MethodInfo("bgm_changed", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::FLOAT, "volume"))); } static const char *phase_name(int p) { switch (p) { case mtnet::PHASE_CLOSE: return "close"; case mtnet::PHASE_HANDSHAKE: return "handshake"; case mtnet::PHASE_LOGIN: return "login"; case mtnet::PHASE_SELECT: return "select"; case mtnet::PHASE_LOADING: return "loading"; case mtnet::PHASE_GAME: return "game"; case mtnet::PHASE_DEAD: return "dead"; case mtnet::PHASE_AUTH: return "auth"; default: return "?"; } } String M2Client::stage_name() const { switch (stage) { case Stage::Idle: return "idle"; case Stage::AuthConnect: return "auth_connect"; case Stage::AuthWait: return "auth_wait"; case Stage::GameConnect: return "game_connect"; case Stage::GameLogin: return "game_login"; case Stage::InGame: return "in_game"; case Stage::Failed: return "failed"; } return "?"; } void M2Client::set_stage(Stage s) { if (stage == s) { return; } stage = s; emit_signal("stage_changed", stage_name()); } bool M2Client::is_in_game() const { return stage == Stage::InGame; } void M2Client::net_poll() { if (suspended) { return; } if (auth) { auth->process(); } if (game) { game->process(); } if (classic_sess) { classic_sess->set_now((uint32_t)Time::get_singleton()->get_ticks_msec()); classic_sess->pump(); } if (mark) { mark->process(); } if (classic_mark) { classic_mark->process(); } } void M2Client::connect_to_server(const String &auth_host, int auth_port, const String &g_host, int g_port, const String &id, const String &pw) { disconnect_from_server(); if (auth_host.is_empty() || g_host.is_empty() || auth_port <= 0 || g_port <= 0 || !mtnet::bounds::u16(auth_port) || !mtnet::bounds::u16(g_port)) { emit_signal("login_failed", String("invalid server address")); set_stage(Stage::Failed); return; } game_host = g_host; game_port = g_port; account_id = id; cfg_auth_host = auth_host; cfg_auth_port = auth_port; cfg_game_host = g_host; cfg_game_port = g_port; cfg_id = id; cfg_pw = pw; have_cfg = true; suspended = false; // MT_PROTOCOL=classic -> 40250 backend: authenticate on auth_host first, // then connect to game_host with the returned login ticket. if (OS::get_singleton()->get_environment("MT_PROTOCOL") == "classic") { classic_sess = std::make_unique(); 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. classic_sess->set_executable_name( std::string(OS::get_singleton()->get_executable_path().get_file().utf8().get_data())); // §2.2 (W1 G1 wiring): non-PC spawn names come from root/npclist.txt. // M2Client has no asset-root handle, so the parser falls back to // $MT_ASSETS/$M2_ASSETS (see npc_names.h default_npclist_path()); an // explicit set_npclist_path() awaits asset-root plumbing into M2Client. if (const String assets = OS::get_singleton()->get_environment("MT_ASSETS"); !assets.is_empty()) { classic_sess->parser().set_npclist_path( std::string(assets.utf8().get_data()) + "/root/npclist.txt"); } // The 40250 server expects CG_ENTERGAME shortly after the loading phase // starts. Keep the live client in the same timing window as m2dev while // allowing a deployment-specific override for unusually large bursts. const String enter_delay = OS::get_singleton()->get_environment("MT_CLASSIC_ENTER_DELAY"); classic_sess->set_auto_entergame_delay(enter_delay.is_empty() ? 1500 : static_cast(MAX(0, enter_delay.to_int()))); last_login_key = 0; classic_list_emitted = false; classic_last_stage = -1; classic_last_empire = -1; shop_open_seen = false; mall_open_seen = false; last_game_phase = -1; if (!classic_sess->connect(std::string(auth_host.utf8().get_data()), (uint16_t)auth_port, std::string(g_host.utf8().get_data()), (uint16_t)g_port, std::string(id.utf8().get_data()), std::string(pw.utf8().get_data()))) { emit_signal("login_failed", String(classic_sess->last_error().c_str())); set_stage(Stage::Failed); classic_sess.reset(); return; } set_stage(Stage::GameConnect); return; } auth = std::make_unique(std::string(id.utf8().get_data()), std::string(pw.utf8().get_data())); char_list_emitted = false; last_auth_state = last_game_state = last_game_phase = last_game_empire = -1; if (!auth->connect(std::string(auth_host.utf8().get_data()), (uint16_t)auth_port)) { emit_signal("login_failed", String(auth->last_error().c_str())); set_stage(Stage::Failed); auth.reset(); return; } set_stage(Stage::AuthConnect); } void M2Client::disconnect_from_server() { if (auth) { auth->disconnect(); auth.reset(); } if (game) { game->disconnect(); game.reset(); } if (classic_sess) { classic_sess->disconnect(); classic_sess.reset(); } if (mark) { mark->disconnect(); mark.reset(); } if (classic_mark) { classic_mark->disconnect(); classic_mark.reset(); } shop_open_seen = false; mall_open_seen = false; selected_pid = 0; if (stage != Stage::Idle) { set_stage(Stage::Idle); } } bool M2Client::select_character(int index) { if (classic_sess) { const bool ok = classic_sess->select_char(index); if (ok && index >= 0 && index < (int)classic_sess->char_slots().size()) { selected_pid = classic_sess->char_slots()[index].id; } return ok; } if (!game) { return false; } const bool ok = game->select_character(index); if (ok) { for (const auto &slot : game->chars()) { if (slot.index == index) { selected_pid = slot.id; break; } } } return ok; } bool M2Client::enter_game(int index) { if (classic_sess) { const bool ok = classic_sess->connect_direct_enter(index); if (ok && index >= 0 && index < static_cast(classic_sess->char_slots().size())) { selected_pid = classic_sess->char_slots()[index].id; } return ok; } return select_character(index); } bool M2Client::create_character(int slot, const godot::String &name, int job, int shape, int con, int intel, int str, int dex) { const std::string name_wire = std::string(name.utf8().get_data()); const size_t name_limit = classic_sess ? mtnet::classic::CHARACTER_NAME_MAX_LEN : mtnet::CHARACTER_NAME_MAX_LEN; if (slot < 0 || slot >= mtnet::PLAYER_PER_ACCOUNT4 || name_wire.empty() || name_wire.size() > name_limit || !mtnet::bounds::u16(job) || !mtnet::bounds::u8(shape) || !mtnet::bounds::u8(con) || !mtnet::bounds::u8(intel) || !mtnet::bounds::u8(str) || !mtnet::bounds::u8(dex)) { return false; } if (classic_sess) { return classic_sess->create_character(slot, name_wire, job, shape, con, intel, str, dex); } if (!game) { return false; } return game->create_character(slot, name_wire, job, shape, con, intel, str, dex); } bool M2Client::delete_character(int slot, const godot::String &private_code) { if (classic_sess) { return classic_sess->delete_character(slot, std::string(private_code.utf8().get_data())); } if (!game) { return false; } return game->delete_character(slot, std::string(private_code.utf8().get_data())); } bool M2Client::change_name(int slot, const godot::String &name) { if (classic_sess) { return classic_sess->change_name(slot, std::string(name.utf8().get_data())); } if (!game) { return false; } return game->change_name(slot, std::string(name.utf8().get_data())); } int M2Client::get_empire() const { if (classic_sess) { return (int)classic_sess->parser().empire(); } return game ? (int)game->empire() : 0; } bool M2Client::select_empire(int empire) { if (empire < 1 || empire > 3) { return false; } if (classic_sess) { return classic_sess->send_empire((uint8_t)empire); } return game && game->phase() == mtnet::PHASE_SELECT && game->send_empire((uint8_t)empire); } int M2Client::get_slot_count() const { if (classic_sess) { return classic_sess->parser().slot_count() > 0 ? classic_sess->parser().slot_count() : mtnet::classic::PLAYER_PER_ACCOUNT; } return game ? game->slot_count() : mtnet::PLAYER_PER_ACCOUNT4; } godot::Array M2Client::build_char_list() const { Array list; if (classic_sess) { const auto &slots = classic_sess->char_slots(); for (int i = 0; i < (int)slots.size(); ++i) { const auto &c = slots[i]; Dictionary d; d["index"] = i; d["id"] = (int64_t)c.id; d["name"] = String::utf8(c.name.c_str()); d["job"] = (int)c.job; d["level"] = (int)c.level; d["x"] = c.x; d["y"] = c.y; d["guild_id"] = (int64_t)c.guild_id; d["guild_name"] = String::utf8(c.guild_name.c_str()); d["play_minutes"] = (int)c.play_minutes; d["st"] = (int)c.st; d["ht"] = (int)c.ht; d["dx"] = (int)c.dx; d["iq"] = (int)c.iq; d["main_part"] = (int)c.main_part; d["hair_part"] = (int)c.hair_part; d["change_name"] = c.change_name; list.push_back(d); } return list; } if (!game) return list; for (const auto &c : game->chars()) { Dictionary d; d["index"] = c.index; d["id"] = (int64_t)c.id; d["name"] = String(c.name.c_str()); d["job"] = c.job; d["level"] = c.level; d["x"] = c.x; d["y"] = c.y; d["guild_id"] = (int64_t)c.guild_id; d["guild_name"] = String::utf8(c.guild_name.c_str()); d["play_minutes"] = (int)c.play_minutes; d["st"] = c.st; d["ht"] = c.ht; d["dx"] = c.dx; d["iq"] = c.iq; d["main_part"] = c.main_part; d["hair_part"] = c.hair_part; d["change_name"] = c.change_name; list.push_back(d); } return list; } // --- in-game intents --- bool M2Client::move(int func, int arg, double rot_deg, int x, int y) { if (!is_in_game() || !mtnet::bounds::u8(func) || !mtnet::bounds::u8(arg) || !valid_angle(rot_deg) || !mtnet::bounds::i32(x) || !mtnet::bounds::i32(y)) { return false; } double r = std::fmod(rot_deg, 360.0); if (r < 0.0) { r += 360.0; } uint32_t t = (uint32_t)Time::get_singleton()->get_ticks_msec(); if (classic_sess) { return classic_sess->send_move((uint8_t)func, (uint8_t)arg, (float)r, (int32_t)x, (int32_t)y, t); } if (!game) { return false; } uint8_t rot = (uint8_t)(r / 5.0); // client wire form: degrees / 5 return game->send_move((uint8_t)func, (uint8_t)arg, rot, (int32_t)x, (int32_t)y, t); } bool M2Client::character_position(int position) { if (!is_in_game() || position < 0 || position > 255) { return false; } if (classic_sess) { return classic_sess->send_character_position((uint8_t)position); } return game && game->send_character_position((uint8_t)position); } bool M2Client::request_warp() { if (classic_sess) { return is_in_game() && classic_sess->send_request_warp(); } return game && is_in_game() && game->send_warp(); } bool M2Client::fishing(double rot_deg) { if (!is_in_game() || !valid_angle(rot_deg)) { return false; } double r = std::fmod(rot_deg, 360.0); if (r < 0.0) { r += 360.0; } if (classic_sess) { return classic_sess->send_fishing((uint8_t)(r / 5.0)); } if (!game) return false; return game->send_fishing((uint8_t)(r / 5.0)); } bool M2Client::request_dungeon() { if (classic_sess) { return is_in_game() && classic_sess->send_dungeon(); } return game && is_in_game() && game->send_dungeon(); } bool M2Client::sync_positions(const Array &positions) { if (!is_in_game() || positions.is_empty() || positions.size() > 16) { return false; } std::vector classic_elements; std::vector elements; if (classic_sess) classic_elements.reserve(positions.size()); else elements.reserve(positions.size()); for (int i = 0; i < positions.size(); ++i) { Variant value = positions[i]; if (value.get_type() != Variant::DICTIONARY) { return false; } Dictionary d = value; if (!d.has("vid") || !d.has("x") || !d.has("y")) { return false; } const Variant raw_vid = d["vid"]; const Variant raw_x = d["x"]; const Variant raw_y = d["y"]; if (raw_vid.get_type() != Variant::INT || raw_x.get_type() != Variant::INT || raw_y.get_type() != Variant::INT) { return false; } const int64_t vid = (int64_t)raw_vid; const int64_t x = (int64_t)raw_x; const int64_t y = (int64_t)raw_y; if (!mtnet::bounds::u32(vid) || !mtnet::bounds::i32(x) || !mtnet::bounds::i32(y)) { return false; } if (classic_sess) classic_elements.push_back({(uint32_t)vid, (int32_t)x, (int32_t)y}); else elements.push_back({(uint32_t)vid, (int32_t)x, (int32_t)y}); } if (classic_sess) return classic_sess->send_sync_positions(classic_elements); if (!game) return false; return game->send_sync_positions(elements); } bool M2Client::attack(int motion, int victim_vid) { if (!is_in_game() || !mtnet::bounds::u8(motion) || victim_vid <= 0 || !mtnet::bounds::u32(victim_vid)) { return false; } if (classic_sess) { return classic_sess->send_attack((uint8_t)motion, (uint32_t)victim_vid); } return game && game->send_attack((uint8_t)motion, (uint32_t)victim_vid); } bool M2Client::set_target(int victim_vid) { // VID 0 is the stock client's explicit "clear target" packet. if (!is_in_game() || victim_vid < 0 || !mtnet::bounds::u32(victim_vid)) { return false; } if (classic_sess) { return classic_sess->send_target((uint32_t)victim_vid); } return game && game->send_target((uint32_t)victim_vid); } bool M2Client::say(int type, const String &text) { if (!is_in_game() || !mtnet::bounds::u8(type)) { return false; } const std::string text_wire = std::string(text.utf8().get_data()); if ((classic_sess && text_wire.size() >= 512) || (!classic_sess && text_wire.size() > 0xffffu - sizeof(mtnet::CGChat) - 1)) { return false; } if (classic_sess) { return classic_sess->send_chat((uint8_t)type, text_wire); } return game && game->send_chat((uint8_t)type, text_wire); } bool M2Client::send_emoticon(int index) { // Registration order from playersettingmodule.py. The reference client's // SendEmoticon sends this token through SendChatPacket, while the received // token is parsed back into the matching visual effect by the chat handler. static constexpr const char *kTokens[] = { "(sweat)", "(money)", "(happy)", "(like)", "(love)", "(angry)", "(aha)", "(sad)", "(sorry)", "(!)", "(?)", "(fish)" }; constexpr int kTokenCount = sizeof(kTokens) / sizeof(kTokens[0]); if (index < 0 || index >= kTokenCount || !say(0 /* CHAT_TYPE_TALKING */, String(kTokens[index]))) { return false; } emit_signal("emoticon_requested", index); return true; } bool M2Client::whisper(const String &to, const String &text) { const std::string to_wire = std::string(to.utf8().get_data()); const std::string text_wire = std::string(text.utf8().get_data()); if (classic_sess) { return is_in_game() && !to_wire.empty() && to_wire.size() <= mtnet::classic::CHARACTER_NAME_MAX_LEN && text_wire.size() < 512 && classic_sess->send_whisper(to_wire, text_wire); } if (!game || !is_in_game() || to_wire.empty() || to_wire.size() > mtnet::CHARACTER_NAME_MAX_LEN || text_wire.size() > 0xffffu - sizeof(mtnet::CGWhisper) - 1) { return false; } return game->send_whisper(to_wire, text_wire); } bool M2Client::cast_skill(int motion_idx, double rot_deg, int x, int y) { // FUNC_SKILL | motion_idx; do not silently mask an invalid caller value. if (motion_idx < 0 || motion_idx > 0x7f) { return false; } return move(0x80 | motion_idx, 0, rot_deg, x, y); } bool M2Client::use_skill(int skill_id, int target_vid) { // §3.8 mod 3: signed params, range-checked before the uint32 wire cast. // skill_id is a real skill index (0 < id < SKILL_MAX_NUM), not a free u32. if (!is_in_game() || !mtnet::bounds::skill_index(skill_id) || target_vid < 0 || !mtnet::bounds::u32(target_vid)) { return false; } if (classic_sess) { return classic_sess->send_use_skill((uint32_t)skill_id, (uint32_t)target_vid); } if (!game) { return false; } if (target_vid > 0) { uint32_t fly_target_vid = 0; int32_t fly_x = 0, fly_y = 0; const mtnet::Entity *target = game->world().get((uint32_t)target_vid); if (target) { fly_target_vid = target->vid; fly_x = (int32_t)target->x; fly_y = (int32_t)target->y; } // The legacy client deliberately falls back to a zero target packet // when the VID is no longer in its character manager. if (!game->send_fly_targeting(fly_target_vid, fly_x, fly_y)) { return false; } } return game->send_use_skill((uint32_t)skill_id, (uint32_t)target_vid); } bool M2Client::shoot(int skill_id) { if (!is_in_game() || skill_id < 0 || skill_id > 255) { return false; } if (classic_sess) return classic_sess->send_shoot((uint8_t)skill_id); if (!game) return false; return game->send_shoot((uint8_t)skill_id); } bool M2Client::add_fly_targeting(int target_vid, int x, int y) { if (!is_in_game() || target_vid < 0 || !mtnet::bounds::u32(target_vid) || !mtnet::bounds::i32(x) || !mtnet::bounds::i32(y)) { return false; } if (classic_sess) return classic_sess->send_add_fly_targeting((uint32_t)target_vid, (int32_t)x, (int32_t)y); if (!game) return false; return game->send_add_fly_targeting((uint32_t)target_vid, (int32_t)x, (int32_t)y); } bool M2Client::skill_up(int skill_id) { // §3.9 / §3.10: /skillup 只发正的、低于 SKILL_MAX_NUM 的技能索引(与 use_skill 同一谓词)。 if (!is_in_game() || !mtnet::bounds::skill_index(skill_id)) { return false; } return say(0 /*CHAT_TYPE_TALKING*/, String("/skillup ") + String::num_int64(skill_id)); } int M2Client::get_skill_group() const { const mtnet::EntityStore *world = active_world(); return world ? (int)world->skill_group() : 0; } Array M2Client::get_skills() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (int id = 0; id < mtnet::SKILL_MAX_NUM; ++id) { int lv = world->skill_level(id); if (lv > 0) { Dictionary d; d["id"] = id; d["level"] = lv; d["master"] = (int)world->skill_master(id); // 0..3 out.push_back(d); } } return out; } Array M2Client::get_quickslots() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (int pos = 0; pos < mtnet::QUICKSLOT_MAX_NUM; ++pos) { mtnet::QuickSlot qs = world->quickslot(pos); if (qs.type == 0) { continue; } Dictionary d; d["pos"] = pos; d["type"] = (int)qs.type; // 1 inventory / 2 skill / 3 emotion / 4 shop (not executable) d["ref"] = (int)qs.position; // item cell / skill id / ... out.push_back(d); } return out; } bool M2Client::quickslot_add(int pos, int type, int ref) { if (!is_in_game() || !mtnet::bounds::quick_slot(pos) || !mtnet::bounds::u8(type) || type < 1 || type > 3 || !mtnet::bounds::u8(ref)) { return false; } if (classic_sess) return classic_sess->send_quickslot_add((uint8_t)pos, (uint8_t)type, (uint8_t)ref); if (!game) return false; return game->send_quickslot_add((uint8_t)pos, (uint8_t)type, (uint8_t)ref); } bool M2Client::quickslot_del(int pos) { if (!is_in_game() || !mtnet::bounds::quick_slot(pos)) { return false; } if (classic_sess) return classic_sess->send_quickslot_del((uint8_t)pos); if (!game) return false; return game->send_quickslot_del((uint8_t)pos); } bool M2Client::quickslot_swap(int pos, int change_pos) { if (!is_in_game() || !mtnet::bounds::quick_slot(pos) || !mtnet::bounds::quick_slot(change_pos)) { return false; } if (classic_sess) return classic_sess->send_quickslot_swap((uint8_t)pos, (uint8_t)change_pos); if (!game) return false; return game->send_quickslot_swap((uint8_t)pos, (uint8_t)change_pos); } bool M2Client::click_npc(int vid) { if (!is_in_game() || vid <= 0 || !mtnet::bounds::u32(vid)) { return false; } if (classic_sess) { return classic_sess->send_click_npc((uint32_t)vid); } return game && game->send_on_click((uint32_t)vid); } bool M2Client::script_answer(int answer) { if (!is_in_game() || !mtnet::bounds::u8(answer)) { return false; } if (classic_sess) { return classic_sess->send_script_answer((uint8_t)answer); } return game && game->send_script_answer((uint8_t)answer); } bool M2Client::script_button(int idx) { if (!is_in_game() || idx < 0 || !mtnet::bounds::u32(idx)) { return false; } if (classic_sess) return classic_sess->send_script_button((uint32_t)idx); return game && game->send_script_button((uint32_t)idx); } bool M2Client::script_select_item(int selection) { if (!is_in_game() || selection < 0 || !mtnet::bounds::u32(selection)) { return false; } if (classic_sess) return classic_sess->send_script_select_item((uint32_t)selection); if (!game) return false; return game->send_script_select_item((uint32_t)selection); } bool M2Client::quest_input(const String &text) { if (classic_sess) return is_in_game() && classic_sess->send_quest_input(std::string(text.utf8().get_data())); return game && is_in_game() && game->send_quest_input(std::string(text.utf8().get_data())); } bool M2Client::quest_confirm(bool yes, int request_pid) { if (classic_sess) return is_in_game() && request_pid >= 0 && mtnet::bounds::u32(request_pid) && classic_sess->send_quest_confirm(yes, (uint32_t)request_pid); return game && is_in_game() && request_pid >= 0 && mtnet::bounds::u32(request_pid) && game->send_quest_confirm(yes, (uint32_t)request_pid); } bool M2Client::quest_cancel() { if (classic_sess) return is_in_game() && classic_sess->send_quest_cancel(); return game && is_in_game() && game->send_quest_cancel(); } Array M2Client::get_quests() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (uint16_t idx : world->quest_indices()) { const mtnet::QuestInfo *q = world->quest(idx); if (!q) { continue; } Dictionary d; d["index"] = (int)q->index; d["begin"] = q->begin; d["title"] = String::utf8(q->title.c_str()); d["clock_name"] = String::utf8(q->clock_name.c_str()); d["clock_value"] = q->clock_value; d["counter_name"] = String::utf8(q->counter_name.c_str()); d["counter_value"] = q->counter_value; d["icon"] = String::utf8(q->icon.c_str()); out.push_back(d); } return out; } // --- P8 social / shop / storage --- bool M2Client::party_invite(int vid) { if (classic_sess) return is_in_game() && vid > 0 && mtnet::bounds::u32(vid) && classic_sess->send_party_invite((uint32_t)vid); return game && is_in_game() && vid > 0 && mtnet::bounds::u32(vid) && game->send_party_invite((uint32_t)vid); } bool M2Client::party_answer(int leader_pid, bool accept) { if (classic_sess) return is_in_game() && leader_pid > 0 && mtnet::bounds::u32(leader_pid) && classic_sess->send_party_answer((uint32_t)leader_pid, accept); return game && is_in_game() && leader_pid > 0 && mtnet::bounds::u32(leader_pid) && game->send_party_invite_answer((uint32_t)leader_pid, accept); } bool M2Client::party_leave(int pid) { if (classic_sess) return is_in_game() && pid > 0 && mtnet::bounds::u32(pid) && classic_sess->send_party_remove((uint32_t)pid); return game && is_in_game() && pid > 0 && mtnet::bounds::u32(pid) && game->send_party_remove((uint32_t)pid); } bool M2Client::party_use_skill(int skill_index, int target_vid) { if (!is_in_game() || !mtnet::bounds::u8(skill_index) || target_vid < 0 || !mtnet::bounds::u32(target_vid)) { return false; } if (classic_sess) return classic_sess->send_party_use_skill((uint8_t)skill_index, (uint32_t)target_vid); if (!game) return false; return game->send_party_use_skill((uint8_t)skill_index, (uint32_t)target_vid); } bool M2Client::party_set_distribute(int mode) { if (classic_sess) return is_in_game() && mtnet::bounds::u8(mode) && classic_sess->send_party_parameter((uint8_t)mode); return game && is_in_game() && mtnet::bounds::u8(mode) && game->send_party_parameter((uint8_t)mode); } bool M2Client::party_set_state(int pid, int role, bool on) { if (!is_in_game() || pid <= 0 || !mtnet::bounds::u32(pid) || role < 0 || role >= 8) { return false; } if (classic_sess) return classic_sess->send_party_set_state((uint32_t)pid, (uint8_t)role, on); return game && game->send_party_set_state((uint32_t)pid, (uint8_t)role, on ? 1 : 0); } Array M2Client::get_party() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (uint32_t pid : world->party_pids()) { const mtnet::PartyMember *m = world->party_member(pid); if (!m) { continue; } Dictionary d; d["pid"] = (int)m->pid; d["vid"] = (int)m->vid; d["name"] = String::utf8(m->name.c_str()); d["leader"] = m->leader(); d["state"] = (int)m->state; // full role byte (bit0 = leader on this fork's server) d["hp_pct"] = (int)m->hp_pct; Array affects; for (int i = 0; i < mtnet::PARTY_AFFECT_SLOT_MAX_NUM; ++i) { affects.push_back((int)m->affects[i]); } d["affects"] = affects; out.push_back(d); } return out; } int M2Client::get_party_distribute_mode() const { const mtnet::EntityStore *world = active_world(); return world ? (int)world->party_distribute_mode() : 0; } bool M2Client::add_friend(const String &name) { if (classic_sess) { return is_in_game() && classic_sess->send_friend_add(std::string(name.utf8().get_data())); } return game && is_in_game() && game->send_friend_add(std::string(name.utf8().get_data())); } bool M2Client::remove_friend(const String &name) { if (classic_sess) { return is_in_game() && classic_sess->send_friend_remove(std::string(name.utf8().get_data())); } return game && is_in_game() && game->send_friend_remove(std::string(name.utf8().get_data())); } bool M2Client::friend_answer(const String &name, bool accept) { if (!is_in_game() || name.is_empty()) { return false; } // 40250 handles messenger invitations through the normal slash-command // channel: `/messenger_auth y|n `, not a CG subheader. const std::string command = "/messenger_auth " + std::string(accept ? "y " : "n ") + std::string(name.utf8().get_data()); if (classic_sess) { return classic_sess->send_chat(mtnet::CHAT_TYPE_TALKING, command); } return game && game->send_chat(mtnet::CHAT_TYPE_TALKING, command); } Array M2Client::get_friends() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (const auto &f : world->friends()) { Dictionary d; d["name"] = String::utf8(f.name.c_str()); d["online"] = f.online; d["mobile"] = f.mobile; out.push_back(d); } return out; } Dictionary M2Client::get_lover() const { Dictionary d; const mtnet::EntityStore *world = active_world(); if (!world) return d; const mtnet::LoverInfo &lover = world->lover(); d["valid"] = lover.valid; d["name"] = String::utf8(lover.name.c_str()); d["love_point"] = (int)lover.love_point; d["online"] = lover.online; d["near"] = lover.near; return d; } bool M2Client::shop_buy(int pos, int count) { if (!is_in_game() || !mtnet::bounds::u8(pos) || count < 1 || !mtnet::bounds::u8(count)) { return false; } if (classic_sess) return classic_sess->send_shop_buy((uint8_t)pos, (uint8_t)count); return game && game->send_shop_buy((uint8_t)pos, (uint8_t)count); } bool M2Client::shop_sell(int inv_cell, int count) { if (!is_in_game() || !mtnet::bounds::u8(inv_cell) || count < 0 || !mtnet::bounds::u8(count)) { return false; } if (classic_sess) return classic_sess->send_shop_sell((uint8_t)inv_cell, (uint8_t)count); return game->send_shop_sell((uint8_t)inv_cell, (uint8_t)count); } bool M2Client::shop_close() { if (classic_sess) return is_in_game() && classic_sess->send_shop_close(); return game && is_in_game() && game->send_shop_close(); } bool M2Client::is_shop_open() const { const mtnet::EntityStore *world = active_world(); return world && world->shop_open(); } static Dictionary shop_entry_dict(const mtnet::ShopEntry &s) { Dictionary d; d["pos"] = (int)s.pos; d["vnum"] = (int)s.vnum; d["price"] = (int)s.price; d["count"] = (int)s.count; return d; } Array M2Client::get_shop_items() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (const auto &s : world->shop_items()) { out.push_back(shop_entry_dict(s)); } return out; } Dictionary M2Client::get_shop() const { Dictionary d; const mtnet::EntityStore *wp = active_world(); if (!wp) { return d; } const mtnet::EntityStore &w = *wp; d["vid"] = (int)w.shop_vid(); d["open"] = w.shop_open(); d["selling_price"] = w.shop_selling_price(); Array tabs; if (!w.shop_tabs().empty()) { // SHOP_GC_START_EX — one entry per shelf, each with its name + coin_type. for (const auto &t : w.shop_tabs()) { Dictionary td; td["name"] = String::utf8(t.name.c_str()); td["coin_type"] = (int)t.coin_type; Array items; for (const auto &s : t.items) { items.push_back(shop_entry_dict(s)); } td["items"] = items; tabs.push_back(td); } } else if (w.shop_open()) { // plain SHOP_GC_START — a single unnamed tab Dictionary td; td["name"] = String(); td["coin_type"] = 0; td["items"] = get_shop_items(); tabs.push_back(td); } d["tabs"] = tabs; return d; } bool M2Client::exchange_start(int vid) { if (!is_in_game() || !mtnet::bounds::u32(vid) || vid <= 0) { return false; } if (classic_sess) { return classic_sess->send_exchange_start((uint32_t)vid); } return game && game->send_exchange_start((uint32_t)vid); } bool M2Client::exchange_add_item(int inv_window, int inv_cell, int display_pos) { if (!is_in_game() || !valid_item_pos(inv_window, inv_cell) || !mtnet::bounds::exchange_slot(display_pos)) { return false; } if (classic_sess) { return classic_sess->send_exchange_item_add((uint8_t)inv_window, (uint16_t)inv_cell, (uint8_t)display_pos); } return game && game->send_exchange_item_add((uint8_t)inv_window, (uint16_t)inv_cell, (uint8_t)display_pos); } bool M2Client::exchange_add_gold(int gold) { if (!is_in_game() || !mtnet::bounds::u32(gold)) { return false; } if (classic_sess) { return classic_sess->send_exchange_gold((uint32_t)gold); } return game && game->send_exchange_gold((uint32_t)gold); } bool M2Client::exchange_accept() { if (classic_sess) { return is_in_game() && classic_sess->send_exchange_accept(); } return game && is_in_game() && game->send_exchange_accept(); } bool M2Client::exchange_cancel() { if (classic_sess) { return is_in_game() && classic_sess->send_exchange_cancel(); } return game && is_in_game() && game->send_exchange_cancel(); } Dictionary M2Client::get_exchange() const { Dictionary d; const mtnet::EntityStore *world = active_world(); if (!world) { return d; } const mtnet::ExchangeState &x = world->exchange(); d["active"] = x.active; d["partner_vid"] = (int)x.partner_vid; d["self_gold"] = (int64_t)x.self_gold; d["peer_gold"] = (int64_t)x.peer_gold; d["self_accept"] = x.self_accept; d["peer_accept"] = x.peer_accept; // EXCHANGE_GC_ALREADY(6)/LESS_GOLD(7) 的子头值;0 = 无。成功/取消的区分 // 只看 CHAT_TYPE_INFO 文案(服务器两种结局都发 GC_END)。 d["notice"] = (int)x.last_notice; auto slots = [](const mtnet::ExchangeSlot *arr) { Array a; for (int i = 0; i < 12; ++i) { if (arr[i].vnum == 0) { continue; } Dictionary s; s["slot"] = i; s["vnum"] = (int)arr[i].vnum; s["count"] = (int)arr[i].count; Array sockets; for (int j = 0; j < mtnet::ITEM_SOCKET_SLOT_MAX_NUM; ++j) { sockets.push_back(arr[i].sockets[j]); } s["sockets"] = sockets; Array attrs; for (int j = 0; j < mtnet::ITEM_ATTRIBUTE_SLOT_MAX_NUM; ++j) { if (arr[i].attrs[j].type == 0) { continue; } Dictionary a; a["type"] = (int)arr[i].attrs[j].type; a["value"] = (int)arr[i].attrs[j].value; attrs.push_back(a); } s["attrs"] = attrs; a.push_back(s); } return a; }; d["self_items"] = slots(x.self_items); d["peer_items"] = slots(x.peer_items); return d; } bool M2Client::safebox_checkin(int safe_pos, int inv_window, int inv_cell) { if (!is_in_game() || !mtnet::bounds::u8(safe_pos) || !valid_item_pos(inv_window, inv_cell)) { return false; } if (classic_sess) { return classic_sess->send_safebox_checkin((uint8_t)safe_pos, (uint8_t)inv_window, (uint16_t)inv_cell); } return game && game->send_safebox_checkin((uint8_t)safe_pos, (uint8_t)inv_window, (uint16_t)inv_cell); } bool M2Client::safebox_checkout(int safe_pos, int inv_window, int inv_cell) { if (!is_in_game() || !mtnet::bounds::u8(safe_pos) || !valid_item_pos(inv_window, inv_cell)) { return false; } if (classic_sess) { return classic_sess->send_safebox_checkout((uint8_t)safe_pos, (uint8_t)inv_window, (uint16_t)inv_cell); } return game && game->send_safebox_checkout((uint8_t)safe_pos, (uint8_t)inv_window, (uint16_t)inv_cell); } bool M2Client::safebox_move(int from_cell, int to_cell, int count) { if (!is_in_game() || !mtnet::bounds::u16(from_cell) || !mtnet::bounds::u16(to_cell) || !mtnet::bounds::u8(count)) { return false; } if (classic_sess) { return classic_sess->send_safebox_move((uint16_t)from_cell, (uint16_t)to_cell, (uint8_t)count); } return game && game->send_safebox_move((uint16_t)from_cell, (uint16_t)to_cell, (uint8_t)count); } bool M2Client::is_safebox_open() const { const mtnet::EntityStore *world = active_world(); return world && world->safebox_open(); } int M2Client::get_safebox_size() const { const mtnet::EntityStore *world = active_world(); return world ? world->safebox_size() : 0; } int M2Client::get_safebox_gold() const { const mtnet::EntityStore *world = active_world(); return world ? (int)world->safebox_gold() : 0; } Array M2Client::get_safebox_items() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (int c = 0; c < mtnet::SAFEBOX_MAX_NUM; ++c) { const mtnet::Item &it = world->safebox_slot(c); if (it.empty()) { continue; } Dictionary d; d["cell"] = c; d["vnum"] = (int)it.vnum; d["count"] = (int)it.count; out.push_back(d); } return out; } // --- item-mall (창고몰) --- bool M2Client::is_mall_open() const { const mtnet::EntityStore *world = active_world(); return world && world->mall_open(); } int M2Client::get_mall_size() const { const mtnet::EntityStore *world = active_world(); return world ? world->mall_size() : 0; } Array M2Client::get_mall_items() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (int c = 0; c < mtnet::MALL_MAX_NUM; ++c) { const mtnet::Item &it = world->mall_slot(c); if (it.empty()) { continue; } Dictionary d; d["cell"] = c; d["vnum"] = (int)it.vnum; d["count"] = (int)it.count; out.push_back(d); } return out; } bool M2Client::mall_checkout(int mall_pos, int inv_window, int inv_cell) { if (!is_in_game() || !mtnet::bounds::u8(mall_pos) || !valid_item_pos(inv_window, inv_cell)) { return false; } if (classic_sess) { return classic_sess->send_mall_checkout((uint8_t)mall_pos, (uint8_t)inv_window, (uint16_t)inv_cell); } return game && game->send_mall_checkout((uint8_t)mall_pos, (uint8_t)inv_window, (uint16_t)inv_cell); } bool M2Client::safebox_password(const String &password) { if (password.is_empty() || password.length() > 6) { return false; } return say(0, String("/safebox_password ") + password); } bool M2Client::mall_password(const String &password) { if (password.is_empty() || password.length() > 6) { return false; } return say(0, String("/mall_password ") + password); } bool M2Client::safebox_change_password(const String &old_password, const String &new_password) { // 服务器 do_safebox_change_password 要求两个参数各 1..6 字符。 if (old_password.is_empty() || old_password.length() > 6 || new_password.is_empty() || new_password.length() > 6) { return false; } return say(0, String("/safebox_change_password ") + old_password + " " + new_password); } bool M2Client::safebox_close() { return say(0, "/safebox_close"); } bool M2Client::mall_close() { return say(0, "/mall_close"); } // --- private (PC) shop --- bool M2Client::open_private_shop(const godot::String &sign, const godot::Array &items) { if (!is_in_game() || (!game && !classic_sess) || items.size() > mtnet::bounds::PRIVATE_SHOP_ITEM_COUNT) { return false; } const std::string sign_wire = std::string(sign.utf8().get_data()); if (sign_wire.size() > mtnet::SHOP_SIGN_MAX_LEN) { return false; } struct PrivateShopItemInput { uint32_t vnum; uint8_t count; uint8_t inv_window; uint16_t inv_cell; uint32_t price; uint8_t display_pos; }; std::vector parsed; parsed.reserve(items.size()); for (int i = 0; i < items.size(); ++i) { const Variant raw = items[i]; if (raw.get_type() != Variant::DICTIONARY) { return false; } const Dictionary d = raw; int64_t vnum = 0; int64_t count = 1; int64_t inv_window = mtnet::WINDOW_INVENTORY; int64_t inv_cell = 0; int64_t price = 0; int64_t display_pos = i; if (!read_int(d, "vnum", vnum, 0) || !read_int(d, "count", count, 1) || !read_int(d, "inv_window", inv_window, mtnet::WINDOW_INVENTORY) || !read_int(d, "inv_cell", inv_cell, 0) || !read_int(d, "price", price, 0) || !read_int(d, "display_pos", display_pos, i) || !mtnet::bounds::u32(vnum) || !mtnet::bounds::u8(count) || count < 1 || !mtnet::bounds::u8(inv_window) || !mtnet::bounds::u16(inv_cell) || !mtnet::bounds::u32(price) || display_pos < 0 || !mtnet::bounds::shop_slot(display_pos)) { return false; } parsed.push_back({(uint32_t)vnum, (uint8_t)count, (uint8_t)inv_window, (uint16_t)inv_cell, (uint32_t)price, (uint8_t)display_pos}); } if (classic_sess) { std::vector v; v.reserve(parsed.size()); for (const auto &in : parsed) { mtnet::classic::CGMyShopItem e{}; e.vnum = in.vnum; e.count = in.count; e.pos = {in.inv_window, in.inv_cell}; e.price = in.price; e.display_pos = in.display_pos; v.push_back(e); } return classic_sess->send_private_shop(sign_wire, v); } std::vector v; v.reserve(parsed.size()); for (const auto &in : parsed) { mtnet::MyShopItem e{}; e.vnum = in.vnum; e.count = in.count; e.pos = {in.inv_window, in.inv_cell}; e.price = in.price; e.display_pos = in.display_pos; v.push_back(e); } return game->send_open_private_shop(sign_wire, v); } bool M2Client::close_private_shop() { if (classic_sess) { // 40250 toggles a player's own shop in CInputMain::MyShop: receiving a // CG_MYSHOP head while GetMyShop() is set closes it. CG_SHOP/END only // stops an NPC-shop visit and leaves a private shop open. return is_in_game() && classic_sess->send_private_shop("", {}); } return game && game->send_close_private_shop(); } // --- cube (제작) --- Dictionary M2Client::get_cube() const { Dictionary d; const mtnet::EntityStore *world = active_world(); if (!world) { return d; } const mtnet::EntityStore::CubeState &c = world->cube(); d["open"] = c.open; d["npc_vnum"] = (int)c.npc_vnum; d["need_gold"] = (int64_t)c.need_gold; d["need_item_vnum"] = (int)c.need_item_vnum; d["need_item_count"] = c.need_item_count; Array recipes; for (size_t ri = 0; ri < c.recipes.size(); ++ri) { const auto &r = c.recipes[ri]; Dictionary rd; rd["index"] = (int)ri; rd["result_vnum"] = (int)r.result_vnum; rd["result_count"] = r.result_count; rd["gold"] = (int64_t)r.gold; Array groups; for (const auto &g : r.material_groups) { Array slots; for (const auto &s : g) { Dictionary sd; sd["vnum"] = (int)s.vnum; sd["count"] = s.count; slots.push_back(sd); } groups.push_back(slots); } rd["material_groups"] = groups; recipes.push_back(rd); } d["recipes"] = recipes; Array results; for (const auto &e : c.results) { Dictionary ed; ed["vnum"] = (int)e.vnum; ed["count"] = e.count; results.push_back(ed); } d["results"] = results; return d; } bool M2Client::cube_make(int result_index) { if (!is_in_game() || result_index < 0 || !mtnet::bounds::u32(result_index)) { return false; } if (classic_sess) return classic_sess->send_cube_make(result_index); return game && game->send_cube_make(result_index); } bool M2Client::cube_request_result_list(int npc_vnum) { if (!is_in_game() || !mtnet::bounds::u32(npc_vnum)) { return false; } if (classic_sess) return classic_sess->send_cube_result_list(npc_vnum); return game && game->send_cube_result_list((uint32_t)npc_vnum); } bool M2Client::cube_request_materials(int start_index, int count) { if (!is_in_game() || !mtnet::bounds::u8(start_index) || count < 1 || !mtnet::bounds::u8(count)) { return false; } if (classic_sess) return classic_sess->send_cube_materials(start_index, count); return game && game->send_cube_material_info(start_index, count); } bool M2Client::cube_open() { if (classic_sess) return is_in_game() && classic_sess->send_cube_open(); return game && is_in_game() && game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube open"); } bool M2Client::cube_close() { if (classic_sess) return is_in_game() && classic_sess->send_cube_close(); return game && is_in_game() && game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube close"); } bool M2Client::cube_list() { if (classic_sess) return is_in_game() && classic_sess->send_cube_list(); return game && is_in_game() && game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube list"); } bool M2Client::cube_add_item(int cube_index, int inventory_index) { if (!is_in_game() || !mtnet::bounds::cube_slot(cube_index) || !mtnet::bounds::u8(inventory_index)) { return false; } if (classic_sess) return classic_sess->send_cube_add_item(cube_index, inventory_index); if (!game) return false; return game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube add " + std::to_string(cube_index) + " " + std::to_string(inventory_index)); } bool M2Client::cube_delete_item(int cube_index) { if (!is_in_game() || !mtnet::bounds::cube_slot(cube_index)) return false; if (classic_sess) return classic_sess->send_cube_delete_item(cube_index); return game && game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube delete " + std::to_string(cube_index)); } // --- guild / refine --- Dictionary M2Client::get_guild() const { Dictionary d; const mtnet::EntityStore *world = active_world(); if (!world) { return d; } const mtnet::GuildState &g = world->guild(); d["in_guild"] = g.in_guild; d["id"] = (int)g.id; d["name"] = String::utf8(g.name.c_str()); d["level"] = (int)g.level; d["exp"] = (int64_t)g.exp; d["gold"] = (int64_t)g.gold; d["member_count"] = (int)g.member_count; d["max_member_count"] = (int)g.max_member_count; d["master_pid"] = (int)g.master_pid; d["has_land"] = g.has_land; return d; } Array M2Client::get_guild_members() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (const auto &m : world->guild_members()) { Dictionary d; d["pid"] = (int)m.pid; d["name"] = String::utf8(m.name.c_str()); d["grade"] = (int)m.grade; d["job"] = (int)m.job; d["level"] = (int)m.level; d["offer"] = (int64_t)m.offer; d["general"] = m.is_general; d["online"] = m.online; out.push_back(d); } return out; } Array M2Client::get_guild_grades() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (int i = 0; i < 16; ++i) { const mtnet::GuildGrade &g = world->guild_grade(i); Dictionary d; d["name"] = String::utf8(g.name.c_str()); d["auth"] = (int)g.auth; out.push_back(d); } return out; } bool M2Client::guild_add_member(int vid) { if (classic_sess) return is_in_game() && vid > 0 && mtnet::bounds::u32(vid) && classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_ADD_MEMBER, (uint32_t)vid); return game && is_in_game() && vid > 0 && mtnet::bounds::u32(vid) && game->send_guild_sub_u32(mtnet::GUILD_CG_ADD_MEMBER, (uint32_t)vid); } bool M2Client::guild_remove_member(int pid) { if (classic_sess) return is_in_game() && pid > 0 && mtnet::bounds::u32(pid) && classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_REMOVE_MEMBER, (uint32_t)pid); return game && is_in_game() && pid > 0 && mtnet::bounds::u32(pid) && game->send_guild_sub_u32(mtnet::GUILD_CG_REMOVE_MEMBER, (uint32_t)pid); } bool M2Client::guild_offer(int amount) { if (classic_sess) return is_in_game() && mtnet::bounds::u32(amount) && classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_OFFER, (uint32_t)amount); return game && is_in_game() && mtnet::bounds::u32(amount) && game->send_guild_sub_u32(mtnet::GUILD_CG_OFFER, (uint32_t)amount); } bool M2Client::guild_deposit_money(int amount) { // 40250 服务器禁用了存款处理(input_main.cpp GUILD_CG_DEPOSIT_MONEY 直接 // return);原版 UI 仍发送该包,这里保持同样的线上行为。 if (classic_sess) return is_in_game() && mtnet::bounds::u32(amount) && classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_DEPOSIT_MONEY, (uint32_t)amount); return game && is_in_game() && mtnet::bounds::u32(amount) && game->send_guild_sub_u32(mtnet::GUILD_CG_DEPOSIT_MONEY, (uint32_t)amount); } bool M2Client::guild_withdraw_money(int amount) { // 同上:40250 服务器禁用取款(且原逻辑仅会长可取,上限 500000)。 if (classic_sess) return is_in_game() && mtnet::bounds::u32(amount) && classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_WITHDRAW_MONEY, (uint32_t)amount); return game && is_in_game() && mtnet::bounds::u32(amount) && game->send_guild_sub_u32(mtnet::GUILD_CG_WITHDRAW_MONEY, (uint32_t)amount); } bool M2Client::guild_charge_gsp(int amount) { if (!mtnet::bounds::i32(amount)) return false; if (classic_sess) return is_in_game() && classic_sess->send_guild_i32(mtnet::classic::GUILD_CG_CHARGE_GSP, (int32_t)amount); return game && is_in_game() && game->send_guild_sub_i32(mtnet::GUILD_CG_CHARGE_GSP, (int32_t)amount); } bool M2Client::guild_change_grade_name(int grade, const String &name) { const std::string value = name.utf8().get_data(); if (grade < 1 || grade >= 16 || value.empty() || value.size() > mtnet::classic::GUILD_GRADE_NAME_MAX_LEN) return false; if (classic_sess) return is_in_game() && classic_sess->send_guild_grade_name((uint8_t)grade, value); return game && is_in_game() && game->send_guild_grade_name((uint8_t)grade, value); } bool M2Client::guild_change_grade_authority(int grade, int authority) { if (grade < 1 || grade >= 16 || !mtnet::bounds::u8(authority)) return false; if (classic_sess) return is_in_game() && classic_sess->send_guild_grade_authority((uint8_t)grade, (uint8_t)authority); return game && is_in_game() && game->send_guild_grade_authority((uint8_t)grade, (uint8_t)authority); } bool M2Client::guild_change_member_grade(int pid, int grade) { if (pid <= 0 || !mtnet::bounds::u32(pid) || grade < 1 || grade >= 16) return false; if (classic_sess) return is_in_game() && classic_sess->send_guild_member_grade((uint32_t)pid, (uint8_t)grade); return game && is_in_game() && game->send_guild_member_grade((uint32_t)pid, (uint8_t)grade); } bool M2Client::guild_change_member_general(int pid, bool enabled) { if (pid <= 0 || !mtnet::bounds::u32(pid)) return false; if (classic_sess) return is_in_game() && classic_sess->send_guild_member_general((uint32_t)pid, enabled); return game && is_in_game() && game->send_guild_member_general((uint32_t)pid, enabled); } bool M2Client::guild_post_comment(const String &text) { const std::string comment = text.utf8().get_data(); if (comment.empty() || comment.size() + 1 > mtnet::classic::GUILD_COMMENT_MAX_LEN) return false; if (classic_sess) return is_in_game() && classic_sess->send_guild_comment(comment); return game && is_in_game() && game->send_guild_comment(comment); } bool M2Client::guild_delete_comment(int comment_id) { if (comment_id <= 0 || !mtnet::bounds::u32(comment_id)) return false; if (classic_sess) return is_in_game() && classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_DELETE_COMMENT, (uint32_t)comment_id); return game && is_in_game() && game->send_guild_sub_u32(mtnet::GUILD_CG_DELETE_COMMENT, (uint32_t)comment_id); } bool M2Client::guild_refresh_comments() { if (classic_sess) return is_in_game() && classic_sess->send_guild_sub(mtnet::classic::GUILD_CG_REFRESH_COMMENT); return game && is_in_game() && game->send_guild_sub(mtnet::GUILD_CG_REFRESH_COMMENT); } bool M2Client::guild_answer_invite(int guild_id, bool accept) { if (guild_id <= 0 || !mtnet::bounds::u32(guild_id)) return false; if (classic_sess) return is_in_game() && classic_sess->send_guild_invite_answer((uint32_t)guild_id, accept); return game && is_in_game() && game->send_guild_invite_answer((uint32_t)guild_id, accept); } bool M2Client::guild_answer_make(const String &name) { if (classic_sess) return is_in_game() && classic_sess->send_guild_answer_make(std::string(name.utf8().get_data())); return game && is_in_game() && game->send_guild_answer_make(std::string(name.utf8().get_data())); } Array M2Client::get_guild_comments() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) return out; for (const auto &comment : world->guild_comments()) { Dictionary d; d["id"] = (int64_t)comment.id; d["name"] = String::utf8(comment.name.c_str()); d["content"] = String::utf8(comment.content.c_str()); out.push_back(d); } return out; } // --- guild war / guild skill --- Dictionary M2Client::get_guild_skill() const { Dictionary d; const mtnet::EntityStore *world = active_world(); if (!world) { return d; } const mtnet::GuildSkillState &s = world->guild_skill(); d["valid"] = s.valid; d["skill_point"] = (int)s.skill_point; d["guild_point"] = (int)s.guild_point; d["max_guild_point"] = (int)s.max_guild_point; Array lv; for (int i = 0; i < mtnet::GUILD_SKILL_MAX_NUM; ++i) { lv.push_back((int)s.levels[i]); } d["levels"] = lv; return d; } Array M2Client::get_guild_wars() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } const mtnet::EntityStore &w = *world; for (const auto &p : w.guild_wars()) { Dictionary d; d["src"] = (int64_t)p.src; d["dst"] = (int64_t)p.dst; d["src_name"] = String::utf8(w.guild_name(p.src).c_str()); d["dst_name"] = String::utf8(w.guild_name(p.dst).c_str()); out.push_back(d); } return out; } Dictionary M2Client::get_guild_war() const { Dictionary d; const mtnet::EntityStore *world = active_world(); if (!world) { return d; } const mtnet::GuildWarStatus &g = world->guild_war(); d["opp_guild_id"] = (int64_t)g.opp_guild_id; d["opp_name"] = String::utf8(world->guild_name(g.opp_guild_id).c_str()); d["type"] = (int)g.type; d["state"] = (int)g.state; return d; } String M2Client::get_guild_name(int guild_id) const { const mtnet::EntityStore *world = active_world(); return world ? String::utf8(world->guild_name((uint32_t)guild_id).c_str()) : String(); } bool M2Client::use_guild_skill(int skill_vnum, int target_vid) { if (!is_in_game() || !mtnet::bounds::u32(skill_vnum) || !mtnet::bounds::u32(target_vid)) { return false; } if (classic_sess) return classic_sess->send_guild_skill((uint32_t)skill_vnum, (uint32_t)target_vid); return game && game->send_guild_use_skill((uint32_t)skill_vnum, (uint32_t)target_vid); } bool M2Client::declare_guild_war(const String &guild_name) { return say(0 /*CHAT_TYPE_TALKING*/, String("/war ") + guild_name); } // --- guild marks --- bool M2Client::download_guild_marks(const String &host, int port) { if ((!game && !classic_sess) || port <= 0 || !mtnet::bounds::u16(port) || host.is_empty()) { return false; } mark_host = host; mark_port = port; mark_ready = false; if (mark) { mark->disconnect(); mark.reset(); } if (classic_mark) { classic_mark->disconnect(); classic_mark.reset(); } if (classic_sess) { classic_mark = std::make_unique( classic_sess->parser().handle(), classic_sess->parser().random_key()); if (!classic_mark->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { UtilityFunctions::push_warning("classic guild-mark connect failed: ", String(classic_mark->last_error().c_str())); classic_mark.reset(); return false; } return true; } const uint32_t handle = game->mark_handle(); const uint32_t random_key = game->mark_random_key(); mark = std::make_unique(handle, random_key); if (!mark->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { UtilityFunctions::push_warning("guild-mark connect failed: ", String(mark->last_error().c_str())); mark.reset(); return false; } return true; } bool M2Client::download_guild_symbol(const String &host, int port, int guild_id) { if ((!game && !classic_sess) || port <= 0 || !mtnet::bounds::u16(port) || host.is_empty() || guild_id <= 0 || !mtnet::bounds::u32(guild_id)) { return false; } symbol_guild_id = 0; symbol_data.clear(); mark_host = host; mark_port = port; if (mark) { mark->disconnect(); mark.reset(); } if (classic_mark) { classic_mark->disconnect(); classic_mark.reset(); } if (classic_sess) { classic_mark = std::make_unique( classic_sess->parser().handle(), classic_sess->parser().random_key()); classic_mark->set_download_symbol((uint32_t)guild_id); if (!classic_mark->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { UtilityFunctions::push_warning("classic guild-symbol download connect failed: ", String(classic_mark->last_error().c_str())); classic_mark.reset(); return false; } return true; } const uint32_t handle = game->mark_handle(); const uint32_t random_key = game->mark_random_key(); mark = std::make_unique(handle, random_key); mark->set_download_symbol((uint32_t)guild_id); if (!mark->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { UtilityFunctions::push_warning("guild-symbol download connect failed: ", String(mark->last_error().c_str())); mark.reset(); return false; } return true; } PackedByteArray M2Client::get_guild_symbol() const { PackedByteArray b; if (!symbol_data.empty()) { b.resize((int64_t)symbol_data.size()); std::memcpy(b.ptrw(), symbol_data.data(), symbol_data.size()); } return b; } bool M2Client::are_guild_marks_ready() const { return mark_ready; } Dictionary M2Client::get_mark_server() const { Dictionary d; d["host"] = mark_host; d["port"] = mark_port; return d; } bool M2Client::upload_guild_mark(const String &host, int port, int guild_id, const Ref &img) { if ((!game && !classic_sess) || port <= 0 || !mtnet::bounds::u16(port) || host.is_empty() || guild_id <= 0 || !mtnet::bounds::u32(guild_id) || img.is_null()) { return false; } Ref rgba = img; if (rgba->get_width() != mtnet::GUILD_MARK_WIDTH || rgba->get_height() != mtnet::GUILD_MARK_HEIGHT || rgba->get_format() != Image::FORMAT_RGBA8) { rgba = img->duplicate(); rgba->convert(Image::FORMAT_RGBA8); if (rgba->get_width() != mtnet::GUILD_MARK_WIDTH || rgba->get_height() != mtnet::GUILD_MARK_HEIGHT) { rgba->resize(mtnet::GUILD_MARK_WIDTH, mtnet::GUILD_MARK_HEIGHT, Image::INTERPOLATE_BILINEAR); } } PackedByteArray b = rgba->get_data(); const int want = mtnet::GUILD_MARK_WIDTH * mtnet::GUILD_MARK_HEIGHT * 4; if (b.size() != want) { return false; } mark_host = host; mark_port = port; if (mark) { mark->disconnect(); mark.reset(); } if (classic_mark) { classic_mark->disconnect(); classic_mark.reset(); } if (classic_sess) { classic_mark = std::make_unique( classic_sess->parser().handle(), classic_sess->parser().random_key()); classic_mark->set_upload_mark((uint32_t)guild_id, (const uint32_t *)b.ptr()); if (!classic_mark->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { UtilityFunctions::push_warning("classic guild-mark upload connect failed: ", String(classic_mark->last_error().c_str())); classic_mark.reset(); return false; } return true; } const uint32_t handle = game->mark_handle(); const uint32_t random_key = game->mark_random_key(); mark = std::make_unique(handle, random_key); mark->set_upload_mark((uint32_t)guild_id, (const uint32_t *)b.ptr()); if (!mark->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { UtilityFunctions::push_warning("guild-mark upload connect failed: ", String(mark->last_error().c_str())); mark.reset(); return false; } return true; } bool M2Client::upload_guild_symbol(const String &host, int port, int guild_id, const PackedByteArray &file_bytes) { if ((!game && !classic_sess) || port <= 0 || !mtnet::bounds::u16(port) || host.is_empty() || guild_id <= 0 || !mtnet::bounds::u32(guild_id) || file_bytes.is_empty()) { return false; } std::vector bytes(file_bytes.ptr(), file_bytes.ptr() + file_bytes.size()); mark_host = host; mark_port = port; if (mark) { mark->disconnect(); mark.reset(); } if (classic_mark) { classic_mark->disconnect(); classic_mark.reset(); } if (classic_sess) { classic_mark = std::make_unique( classic_sess->parser().handle(), classic_sess->parser().random_key()); classic_mark->set_upload_symbol((uint32_t)guild_id, std::move(bytes)); if (!classic_mark->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { UtilityFunctions::push_warning("classic guild-symbol upload connect failed: ", String(classic_mark->last_error().c_str())); classic_mark.reset(); return false; } return true; } const uint32_t handle = game->mark_handle(); const uint32_t random_key = game->mark_random_key(); mark = std::make_unique(handle, random_key); mark->set_upload_symbol((uint32_t)guild_id, std::move(bytes)); if (!mark->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { UtilityFunctions::push_warning("guild-symbol upload connect failed: ", String(mark->last_error().c_str())); mark.reset(); return false; } return true; } Dictionary M2Client::get_guild_mark(int guild_id) const { Dictionary d; d["found"] = false; if (!mark_store) { return d; } mtnet::MarkRect rc = mark_store->rect_of((uint32_t)guild_id); d["found"] = rc.found; d["img_idx"] = rc.img_idx; d["x"] = rc.x; d["y"] = rc.y; d["w"] = rc.w; d["h"] = rc.h; return d; } Ref M2Client::get_guild_mark_image(int guild_id) const { Ref img; if (!mark_store) { return img; } std::vector px = mark_store->mark_pixels((uint32_t)guild_id); if (px.empty()) { return img; } PackedByteArray b; b.resize((int64_t)px.size() * 4); std::memcpy(b.ptrw(), px.data(), px.size() * 4); img = Image::create_from_data(mtnet::GUILD_MARK_WIDTH, mtnet::GUILD_MARK_HEIGHT, false, Image::FORMAT_RGBA8, b); return img; } void M2Client::pump_mark() { mark->process(); const bool offline = mark->state() == mtnet::NetStream::State::Offline; if (mark->mode() == mtnet::MarkClient::Mode::DownloadSymbol) { if (mark->complete()) { symbol_guild_id = mark->symbol_guild_id(); symbol_data = mark->symbol_data(); PackedByteArray b = get_guild_symbol(); mark.reset(); emit_signal("guild_symbol_ready", (int64_t)symbol_guild_id, b); } else if (offline) { mark.reset(); } return; } if (mark->mode() != mtnet::MarkClient::Mode::Download) { // upload: done once the packet has actually left our send buffer, or the // server closed after we sent it. if (mark->upload_done() || (offline && mark->upload_sent())) { mark.reset(); emit_signal("guild_mark_uploaded", true); } else if (offline) { mark.reset(); emit_signal("guild_mark_uploaded", false); } return; } if (mark->complete()) { mark_store = std::make_unique(mark->marks()); mark_ready = true; size_t n = mark_store->mark_count(); mark.reset(); emit_signal("guild_marks_ready", (int)n); return; } if (offline) { // server closed before COMPLETE — keep whatever blocks arrived. mark_store = std::make_unique(mark->marks()); mark_ready = mark_store->mark_count() > 0; mark.reset(); emit_signal("guild_marks_ready", mark_ready ? (int)mark_store->mark_count() : 0); } } void M2Client::pump_classic_mark() { classic_mark->process(); const bool offline = classic_mark->state() == mtnet::classic::ClassicStream::State::Offline; if (classic_mark->mode() == mtnet::classic::ClassicMarkClient::Mode::DownloadSymbol) { if (classic_mark->complete()) { symbol_guild_id = classic_mark->symbol_guild_id(); symbol_data = classic_mark->symbol_data(); PackedByteArray b = get_guild_symbol(); classic_mark.reset(); emit_signal("guild_symbol_ready", (int64_t)symbol_guild_id, b); } else if (offline) { classic_mark.reset(); } return; } if (classic_mark->mode() != mtnet::classic::ClassicMarkClient::Mode::Download) { if (classic_mark->upload_done() || (offline && classic_mark->upload_sent())) { classic_mark.reset(); emit_signal("guild_mark_uploaded", true); } else if (offline) { classic_mark.reset(); emit_signal("guild_mark_uploaded", false); } return; } if (classic_mark->complete()) { mark_store = std::make_unique(classic_mark->marks()); mark_ready = true; size_t n = mark_store->mark_count(); classic_mark.reset(); emit_signal("guild_marks_ready", (int)n); return; } if (offline) { // Keep successfully received blocks if the stock mark server closes // immediately after the final block instead of sending PHASE_CLOSE. mark_store = std::make_unique(classic_mark->marks()); mark_ready = mark_store->mark_count() > 0; classic_mark.reset(); emit_signal("guild_marks_ready", mark_ready ? (int)mark_store->mark_count() : 0); } } bool M2Client::refine(int pos, int type) { if (!is_in_game() || pos < 0 || pos >= mtnet::INVENTORY_MAX_NUM || !mtnet::bounds::u8(type)) { return false; } if (classic_sess) return classic_sess->send_refine((uint8_t)pos, (uint8_t)type); return game && game->send_refine((uint8_t)pos, (uint8_t)type); } bool M2Client::ds_refine(int mode, const Array &cells) { if (!is_in_game()) { return false; } static const uint8_t kMode[3] = { mtnet::DS_SUB_DO_UPGRADE, mtnet::DS_SUB_DO_IMPROVEMENT, mtnet::DS_SUB_DO_REFINE }; if (mode < 0 || mode > 2) { return false; } if (classic_sess) { mtnet::classic::ItemPos grid[mtnet::classic::DS_REFINE_GRID_MAX_NUM] = {}; int n = (int)cells.size(); if (n > mtnet::classic::DS_REFINE_GRID_MAX_NUM) return false; for (int i = 0; i < n; ++i) { int cell = (int)cells[i]; if (cell < 0 || cell > 0xffff) return false; grid[i] = {(uint8_t)mtnet::WINDOW_INVENTORY, (uint16_t)cell}; } return classic_sess->send_dragon_soul_refine(kMode[mode], grid, n); } if (!game) return false; mtnet::ItemPos grid[mtnet::DS_REFINE_WINDOW_MAX_NUM] = {}; int n = (int)cells.size(); if (n > mtnet::DS_REFINE_WINDOW_MAX_NUM) return false; for (int i = 0; i < n; ++i) { int cell = (int)cells[i]; if (cell < 0 || cell > 0xffff) return false; grid[i].window_type = mtnet::WINDOW_INVENTORY; grid[i].cell = (uint16_t)cell; } return game->send_ds_refine(kMode[mode], grid); } Array M2Client::get_dragon_souls() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (int c = 0; c < mtnet::DRAGON_SOUL_MAX_NUM; ++c) { const mtnet::Item &it = world->dragon_soul_slot(c); if (it.vnum == 0) { continue; } Dictionary d; d["cell"] = c; d["vnum"] = (int)it.vnum; d["count"] = (int)it.count; out.push_back(d); } return out; } // --- P9 world systems --- int M2Client::get_channel() const { const auto *world = active_world(); return world ? world->channel() : 0; } int64_t M2Client::get_server_time() const { const auto *world = active_world(); return world ? world->server_time() : 0; } Array M2Client::get_npc_marks() const { Array out; const auto *world = active_world(); if (!world) { return out; } for (const auto &m : world->npc_marks()) { Dictionary d; d["type"] = (int)m.type; d["vnum"] = (int)m.vnum; d["name"] = String::utf8(m.name.c_str()); fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(m.x, m.y, 0); d["pos"] = Vector3(g.x, g.y, g.z); out.push_back(d); } return out; } Array M2Client::get_land_areas() const { Array out; const auto *world = active_world(); if (!world) return out; for (const auto &a : world->land_areas()) { Dictionary d; d["id"] = (int)a.id; d["guild_id"] = (int)a.guild_id; d["x"] = a.x; d["y"] = a.y; d["width"] = a.width; d["height"] = a.height; out.push_back(d); } return out; } Array M2Client::get_observers() const { Array out; const auto *world = active_world(); if (!world) return out; for (const auto &o : world->observers()) { Dictionary d; d["vid"] = (int)o.vid; fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(o.x, o.y, 0); d["pos"] = Vector3(g.x, g.y, g.z); out.push_back(d); } return out; } bool M2Client::is_observer_mode() const { const auto *world = active_world(); return world && world->observer_mode(); } int M2Client::get_observer_count() const { const auto *world = active_world(); return world ? world->observer_count() : 0; } bool M2Client::has_mobile_flag() const { const auto *world = active_world(); return world && world->mobile_flag(); } bool M2Client::combo_skill_enabled() const { const auto *world = active_world(); return world && world->combo_skill_flag(); } Dictionary M2Client::get_stamina_state() const { Dictionary d; const auto *world = active_world(); if (!world) return d; d["consuming"] = world->stamina_consuming(); d["per_sec"] = (int)world->stamina_per_sec(); d["current"] = (int)world->current_stamina(); return d; } Array M2Client::get_world_markers() const { Array out; const auto *world = active_world(); if (!world) { return out; } for (const auto &m : world->markers()) { Dictionary d; d["id"] = (int)m.id; d["name"] = String::utf8(m.name.c_str()); d["vid"] = (int)m.vid; d["type"] = (int)m.type; fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(m.x, m.y, 0); d["pos"] = Vector3(g.x, g.y, g.z); out.push_back(d); } return out; } // --- world snapshot --- static Dictionary entity_dict(const mtnet::Entity &e) { Dictionary d; d["vid"] = (int)e.vid; d["race"] = (int)e.race; d["ch_type"] = (int)e.ch_type; // 0 PC, 1 NPC, 2 MONSTER, ... d["name"] = String::utf8(e.name.c_str()); d["shop_sign"] = String::utf8(e.shop_sign.c_str()); d["is_main"] = e.is_main; d["func"] = (int)e.func; d["position"] = (int)e.position; d["walk_mode"] = (int)e.walk_mode; d["moving_speed"] = (int)e.moving_speed; d["attack_speed"] = (int)e.attack_speed; d["fly_target_vid"] = (int)e.fly_target_vid; d["fly_target_set"] = e.fly_target_set; d["fly_target_cm"] = Vector3(e.fly_target_x, 0, e.fly_target_y); d["moving"] = e.moving; d["angle_deg"] = e.angle; d["hp"] = e.hp; d["max_hp"] = e.max_hp; d["sp"] = e.sp; d["max_sp"] = e.max_sp; d["level"] = e.level; d["dead"] = e.dead; d["stunned"] = e.stunned; d["knock_down"] = e.knock_down; d["mount_vnum"] = (int)e.mount_vnum; d["guild"] = e.guild; d["alignment"] = (int)e.alignment; d["pk_mode"] = (int)e.pk_mode; d["empire"] = (int)e.empire; d["affect_flags"] = (int64_t)e.affect_flags; d["owner_vid"] = (int)e.owner_vid; d["state_flags"] = (int)e.state_flags; Array parts; for (int i = 0; i < 4; ++i) { parts.push_back((int)e.parts[i]); } d["parts"] = parts; // server cm (Z-up) -> Godot metres (Y-up) fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(e.x, e.y, e.z); d["pos"] = Vector3(g.x, g.y, g.z); d["pos_cm"] = Vector3(e.x, e.y, e.z); return d; } const mtnet::EntityStore *M2Client::active_world() const { if (classic_sess) { return &classic_sess->world(); } return game ? &game->world() : nullptr; } Dictionary M2Client::get_entity(int vid) const { const mtnet::EntityStore *w = active_world(); if (!w) { return Dictionary(); } const mtnet::Entity *e = w->get((uint32_t)vid); return e ? entity_dict(*e) : Dictionary(); } Array M2Client::get_entities() const { Array out; const mtnet::EntityStore *w = active_world(); if (!w) { return out; } for (uint32_t vid : w->vids()) { if (const mtnet::Entity *e = w->get(vid)) { out.push_back(entity_dict(*e)); } } return out; } int M2Client::get_main_vid() const { const mtnet::EntityStore *w = active_world(); return w ? (int)w->main_vid() : 0; } int M2Client::get_main_pid() const { return (int)selected_pid; } Dictionary M2Client::get_points() const { Dictionary d; const mtnet::EntityStore *w = active_world(); if (!w) { return d; } const mtnet::PlayerPoints &p = w->points(); d["hp"] = p.hp(); d["max_hp"] = p.max_hp(); d["sp"] = p.sp(); d["max_sp"] = p.max_sp(); d["stamina"] = p.stamina(); d["max_stamina"] = p.max_stamina(); d["level"] = p.level(); // exp values overflow int32 at high level on live servers — surface unsigned. d["exp"] = (int64_t)(uint32_t)p.exp(); d["next_exp"] = (int64_t)(uint32_t)p.next_exp(); d["gold"] = p.gold(); d["attack_speed"] = p.v[mtnet::POINT_ATT_SPEED]; d["bow_distance"] = p.v[mtnet::POINT_BOW_DISTANCE]; // __GetBowRange() 距离加成 // §3.10 加号按钮门控:三个技能点来源分开取(uicharacter.py skillPageStatDict)。 d["skill_active"] = p.v[mtnet::POINT_SKILL]; d["skill_support"] = p.v[mtnet::POINT_SUB_SKILL]; d["skill_horse"] = p.v[mtnet::POINT_HORSE_SKILL]; d["energy"] = p.energy(); d["energy_end_time"] = p.energy_end_time(); Array values; for (int i = 0; i < mtnet::POINT_MAX_NUM; ++i) { values.push_back(p.v[i]); } d["points"] = values; return d; } Dictionary M2Client::get_target() const { Dictionary d; const mtnet::EntityStore *world = active_world(); if (!world) { return d; } d["vid"] = (int)world->target_vid(); d["hp_percent"] = (int)world->target_hp_pct(); return d; } static Dictionary affect_dict(const mtnet::Affect &a) { Dictionary d; d["type"] = (int)a.type; d["point_idx"] = (int)a.point_idx; d["value"] = a.value; d["flag"] = (int)a.flag; d["duration"] = a.duration; return d; } Array M2Client::get_affects() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (const auto &a : world->affects()) { out.push_back(affect_dict(a)); } return out; } // --- items --- bool M2Client::move_item(int fw, int fc, int tw, int tc, int count) { if (!is_in_game() || !valid_item_pos(fw, fc) || !valid_item_pos(tw, tc) || !mtnet::bounds::u8(count)) { return false; } if (classic_sess) { return classic_sess->send_item_move((uint8_t)fw, (uint16_t)fc, (uint8_t)tw, (uint16_t)tc, (uint8_t)count); } return game && game->send_item_move((uint8_t)fw, (uint16_t)fc, (uint8_t)tw, (uint16_t)tc, (uint8_t)count); } bool M2Client::use_item(int window, int cell) { if (!is_in_game() || !valid_item_pos(window, cell)) { return false; } if (classic_sess) { return classic_sess->send_item_use((uint8_t)window, (uint16_t)cell); } return game && game->send_item_use((uint8_t)window, (uint16_t)cell); } bool M2Client::drop_item(int window, int cell, int gold) { if (!is_in_game() || !valid_item_pos(window, cell) || !mtnet::bounds::u32(gold)) { return false; } if (classic_sess) { return classic_sess->send_item_drop((uint8_t)window, (uint16_t)cell, (uint32_t)gold, 0); } return game && game->send_item_drop((uint8_t)window, (uint16_t)cell, (uint32_t)gold); } bool M2Client::drop_item_count(int window, int cell, int gold, int count) { if (!is_in_game() || !valid_item_pos(window, cell) || !mtnet::bounds::u32(gold) || count < 1 || !mtnet::bounds::u8(count)) return false; if (classic_sess) { return classic_sess->send_item_drop((uint8_t)window, (uint16_t)cell, (uint32_t)gold, (uint8_t)count); } return game && game->send_item_drop2((uint8_t)window, (uint16_t)cell, (uint32_t)gold, (uint8_t)count); } bool M2Client::use_item_to_item(int sw, int sc, int tw, int tc) { if (!is_in_game() || !valid_item_pos(sw, sc) || !valid_item_pos(tw, tc)) return false; if (classic_sess) { return classic_sess->send_item_use_to_item((uint8_t)sw, (uint16_t)sc, (uint8_t)tw, (uint16_t)tc); } if (!game) return false; return game->send_item_use_to_item((uint8_t)sw, (uint16_t)sc, (uint8_t)tw, (uint16_t)tc); } bool M2Client::give_item(int target_vid, int window, int cell, int count) { if (!is_in_game() || target_vid <= 0 || !mtnet::bounds::u32(target_vid) || !valid_item_pos(window, cell) || count < 1 || !mtnet::bounds::u8(count)) return false; if (classic_sess) { return classic_sess->send_item_give((uint32_t)target_vid, (uint8_t)window, (uint16_t)cell, (uint8_t)count); } if (!game) return false; return game->send_give_item((uint32_t)target_vid, (uint8_t)window, (uint16_t)cell, (uint8_t)count); } bool M2Client::pickup_item(int ground_vid) { if (!is_in_game() || ground_vid <= 0 || !mtnet::bounds::u32(ground_vid)) { return false; } if (classic_sess) { return classic_sess->send_item_pickup((uint32_t)ground_vid); } return game && game->send_item_pickup((uint32_t)ground_vid); } static Dictionary item_dict(const mtnet::Item &it) { Dictionary d; d["vnum"] = (int)it.vnum; d["count"] = (int)it.count; d["flags"] = (int)it.flags; d["anti_flags"] = (int)it.anti_flags; Array sockets; for (int i = 0; i < 3; ++i) { sockets.push_back(it.sockets[i]); } d["sockets"] = sockets; Array attrs; for (int i = 0; i < 7; ++i) { if (it.attrs[i].type == 0) { continue; } Dictionary a; a["type"] = (int)it.attrs[i].type; a["value"] = (int)it.attrs[i].value; attrs.push_back(a); } d["attrs"] = attrs; return d; } Array M2Client::get_inventory() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (int c = 0; c < mtnet::INVENTORY_MAX_NUM; ++c) { const mtnet::Item &it = world->inv_slot(c); if (it.empty()) { continue; } Dictionary d = item_dict(it); d["cell"] = c; out.push_back(d); } return out; } Array M2Client::get_equipment() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (int w = 0; w < mtnet::WEAR_MAX_NUM; ++w) { Dictionary d = item_dict(world->equip_slot(w)); d["wear"] = w; out.push_back(d); } return out; } Array M2Client::get_belt_inventory() const { Array out; const auto *world = active_world(); if (!world) return out; for (int c = 0; c < mtnet::BELT_INVENTORY_MAX_NUM; ++c) { const mtnet::Item &it = world->belt_slot(c); if (it.empty()) continue; Dictionary d = item_dict(it); d["cell"] = c; out.push_back(d); } return out; } Array M2Client::get_view_equipment(int vid) const { Array out; const auto *world = active_world(); if (!world || vid <= 0) return out; const mtnet::ViewedEquipment *view = world->viewed_equipment((uint32_t)vid); if (!view) return out; for (int wear = 0; wear < mtnet::VIEW_EQUIP_WEAR_MAX_NUM; ++wear) { Dictionary d = item_dict(view->items[wear]); d["wear"] = wear; out.push_back(d); } return out; } Dictionary M2Client::get_item(int window, int cell) const { const mtnet::EntityStore *world = active_world(); if (!world) { return Dictionary(); } return item_dict(world->item_slot((uint8_t)window, cell)); } Array M2Client::get_ground_items() const { Array out; const mtnet::EntityStore *world = active_world(); if (!world) { return out; } for (uint32_t vid : world->ground_vids()) { const mtnet::GroundItem *g = world->ground(vid); if (!g) { continue; } Dictionary d; d["vid"] = (int)g->vid; d["vnum"] = (int)g->vnum; d["owner"] = String::utf8(g->owner.c_str()); fmt::m2coord::Vec3 gp = fmt::m2coord::position_to_godot(g->x, g->y, g->z); d["pos"] = Vector3(gp.x, gp.y, gp.z); out.push_back(d); } return out; } Array M2Client::get_pvp_relations() const { Array out; const auto *world = active_world(); if (!world) return out; for (const auto &p : world->pvp_relations()) { Dictionary d; d["src_vid"] = (int)p.src_vid; d["dst_vid"] = (int)p.dst_vid; d["mode"] = (int)p.mode; out.push_back(d); } return out; } Dictionary M2Client::get_duel() const { Dictionary out; const auto *world = active_world(); if (!world) return out; Array opponents; for (uint32_t vid : world->duel_opponents()) { opponents.push_back((int)vid); } out["active"] = !world->duel_opponents().empty(); out["cannot_attack"] = world->duel_cannot_attack(); out["opponents"] = opponents; return out; } void M2Client::_process(double) { if (Engine::get_singleton()->is_editor_hint()) { return; } if (suspended) { return; // backgrounded — don't touch the socket } if (classic_sess) { pump_classic(); } if (auth) { pump_auth(); } if (game) { pump_game(); } if (mark) { pump_mark(); } if (classic_mark) { pump_classic_mark(); } } // §9.3: M2Client no longer listens for NOTIFICATION_APPLICATION_PAUSED/RESUMED. // AppFlow owns the single AppLifecycle coordinator (equivalent to // CPythonApplication); it drives suspend()/resume() below. Keeping a second // listener here meant two entry points for the same lifecycle edge. void M2Client::suspend() { if (suspended) { return; } suspended = true; emit_signal("suspended"); } void M2Client::resume() { if (!suspended) { return; } suspended = false; emit_signal("resumed"); // The next _process() pumps the socket; if the OS dropped the TCP // connection while backgrounded, pump_game()/pump_auth() will see it go // Offline and emit `disconnected`. GDScript can then call reconnect(). } bool M2Client::reconnect() { if (!have_cfg) { return false; } // Fast path: still holding a valid login_key -> skip the auth server, go // straight back to the (last) game server. Falls back to full re-login if // the game connect fails. if (last_login_key != 0 && !cfg_game_host.is_empty()) { warp_to_game_server(cfg_game_host, cfg_game_port); if (stage == Stage::GameConnect) { return true; } } connect_to_server(cfg_auth_host, cfg_auth_port, cfg_game_host, cfg_game_port, cfg_id, cfg_pw); return stage != Stage::Failed && stage != Stage::Idle; } // Tear down the current game connection and reconnect to `host:port` reusing the // stored login_key (cross-server GC_WARP, or a fast reconnect). void M2Client::warp_to_game_server(const String &host, int port) { if (classic_sess) { classic_sess->disconnect(); classic_sess.reset(); if (mark) { mark->disconnect(); mark.reset(); } if (classic_mark) { classic_mark->disconnect(); classic_mark.reset(); } game_host = host; game_port = port; classic_list_emitted = false; classic_last_stage = -1; classic_last_empire = -1; shop_open_seen = false; mall_open_seen = false; dead_seen.clear(); classic_sess = std::make_unique(); 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()), (uint16_t)port, std::string(cfg_id.utf8().get_data()), last_login_key); } else { connected = classic_sess->connect(std::string(cfg_auth_host.utf8().get_data()), (uint16_t)cfg_auth_port, std::string(host.utf8().get_data()), (uint16_t)port, std::string(cfg_id.utf8().get_data()), std::string(cfg_pw.utf8().get_data())); } if (!connected) { emit_signal("login_failed", String(classic_sess->last_error().c_str())); set_stage(Stage::Failed); classic_sess.reset(); return; } last_game_phase = -1; set_stage(Stage::GameConnect); return; } if (last_login_key == 0) { return; } if (game) { game->disconnect(); game.reset(); } auth.reset(); game_host = host; game_port = port; char_list_emitted = false; shop_open_seen = false; dead_seen.clear(); game = std::make_unique(std::string(account_id.utf8().get_data()), last_login_key); game->set_auto_enter_game(false); loading_since_ms = 0; enter_game_sent = false; if (!game->connect(std::string(host.utf8().get_data()), (uint16_t)port)) { emit_signal("login_failed", String(game->last_error().c_str())); set_stage(Stage::Failed); game.reset(); return; } last_game_phase = -1; last_game_state = last_game_empire = -1; set_stage(Stage::GameConnect); } void M2Client::pump_classic() { using St = mtnet::INetSession::Stage; classic_sess->set_now((uint32_t)Time::get_singleton()->get_ticks_msec()); classic_sess->pump(); if (classic_sess->login_key() != 0) { last_login_key = classic_sess->login_key(); } const int empire = (int)classic_sess->parser().empire(); if (empire != classic_last_empire) { classic_last_empire = empire; emit_signal("empire_changed", empire); } // stage -> phase_changed / stage_changed / char_list / entered_game int st = (int)classic_sess->stage(); if (st != classic_last_stage) { classic_last_stage = st; static const char *pn[] = {"close", "handshake", "login", "select", "loading", "in_game", "failed"}; int idx = 0; switch (classic_sess->stage()) { case St::Offline: idx = 0; break; case St::Connecting: idx = 1; break; case St::LoggingIn: idx = 2; break; case St::CharSelect: idx = 3; break; case St::Loading: idx = 4; break; case St::InGame: idx = 5; break; case St::Failed: idx = 6; break; } emit_signal("phase_changed", String(pn[idx])); if (classic_sess->stage() == St::Failed && stage != Stage::Failed) { // ClientVS22 sends every already-established game connection loss back // through SetLoginPhase(), regardless of whether SELECT was reached. // Authentication failures happen before this flag is set and remain // login_failed events. if (classic_sess->was_online_lost()) { emit_signal("disconnected", String(classic_sess->last_error().c_str())); } else { emit_signal("login_failed", String(classic_sess->last_error().c_str())); } set_stage(Stage::Failed); } else if (classic_sess->stage() == St::LoggingIn && stage == Stage::GameConnect) { set_stage(Stage::GameLogin); } } if (!classic_list_emitted && classic_sess->parser().char_list_ready() && classic_sess->stage() == St::CharSelect) { classic_list_emitted = true; emit_signal("char_list", build_char_list()); } for (const auto &ev : classic_sess->parser().drain_char_events()) { using CE = mtnet::classic::ClassicParser::CharEvent; switch (ev.kind) { case CE::CreateOk: emit_signal("char_list", build_char_list()); emit_signal("char_created", ev.slot); break; case CE::CreateFail: emit_signal("char_create_failed", ev.fail_type); break; case CE::DeleteOk: emit_signal("char_list", build_char_list()); emit_signal("char_deleted", ev.slot); break; case CE::DeleteFail: emit_signal("char_delete_failed"); break; } } for (const auto &ev : classic_sess->parser().drain_name_events()) { emit_signal("char_list", build_char_list()); emit_signal("char_name_changed", (int64_t)ev.pid, String::utf8(ev.name.c_str())); } for (int i = 0, n = classic_sess->parser().drain_guild_make_requests(); i < n; ++i) { emit_signal("guild_make_requested"); } if (classic_sess->in_game() && stage != Stage::InGame) { set_stage(Stage::InGame); emit_signal("entered_game"); } if (classic_sess->in_game()) { mtnet::EntityStore &w = classic_sess->world(); // set_now()/tick() already run inside pump(); mirror the deltas the // classic parser currently produces (grows with classic_parser). for (const auto &c : w.drain_changes()) { switch (c.kind) { case mtnet::EntityStore::ChangeKind::Spawn: if (const mtnet::Entity *e = w.get(c.vid)) { emit_signal("entity_spawned", entity_dict(*e)); } break; case mtnet::EntityStore::ChangeKind::Despawn: dead_seen.erase(c.vid); emit_signal("entity_despawned", (int)c.vid); break; case mtnet::EntityStore::ChangeKind::Move: emit_signal("entity_moved", (int)c.vid); break; case mtnet::EntityStore::ChangeKind::MainSet: emit_signal("entity_main_set", (int)c.vid); break; case mtnet::EntityStore::ChangeKind::Info: if (const mtnet::Entity *e = w.get(c.vid)) { emit_signal("entity_info", (int)c.vid, entity_dict(*e)); } break; } } // §2.5: bare field updates (ownership / affect flags) with no Spawn/Move/ // Info change — re-mirror the node so presentation stays in sync. for (uint32_t vid : w.drain_dirty()) { if (const mtnet::Entity *e = w.get(vid)) { emit_signal("entity_info", (int)vid, entity_dict(*e)); } } // §9.1: map background music. if (w.take_bgm_dirty()) { emit_signal("bgm_changed", String::utf8(w.bgm_name().c_str()), w.bgm_volume()); } for (const auto &m : w.drain_chat()) { if (m.type == mtnet::CHAT_TYPE_WHISPER) { emit_signal("whisper_received", (int)m.sub, String::utf8(m.from.c_str()), String::utf8(m.text.c_str())); } else { emit_signal("chat", (int)m.type, (int)m.vid, String::utf8(m.text.c_str())); } } for (uint32_t vid : w.drain_vitals()) { emit_signal("vitals_changed", (int)vid); const mtnet::Entity *e = w.get(vid); if (e && e->dead && !dead_seen.count(vid)) { dead_seen.insert(vid); emit_signal("entity_dead", (int)vid); } } if (w.take_points_dirty()) { emit_signal("points_changed", get_points()); } for (const auto &ic : w.drain_inv()) { emit_signal("inventory_changed", (int)ic.window, (int)ic.cell); } for (const auto &gc : w.drain_ground()) { if (gc.added) { const mtnet::GroundItem *g = w.ground(gc.vid); if (g) { Dictionary d; d["vid"] = (int)g->vid; d["vnum"] = (int)g->vnum; d["owner"] = String::utf8(g->owner.c_str()); fmt::m2coord::Vec3 gp = fmt::m2coord::position_to_godot(g->x, g->y, g->z); d["pos"] = Vector3(gp.x, gp.y, gp.z); emit_signal("ground_item_added", d); } } else { emit_signal("ground_item_removed", (int)gc.vid); } } for (const auto &p : w.drain_pvp_changes()) { emit_signal("pvp_changed", (int)p.src_vid, (int)p.dst_vid, (int)p.mode); } if (w.take_duel_started()) { emit_signal("duel_changed", get_duel()); emit_signal("duel_started"); } for (const auto &mo : w.drain_motions()) { emit_signal("motion", (int)mo.vid, (int)mo.victim_vid, (int)mo.motion); } for (const auto &dm : w.drain_dig_motions()) { emit_signal("dig_motion", (int)dm.vid, (int)dm.target_vid, (int)dm.count); } for (const auto &fe : w.drain_fishing_events()) { emit_signal("fishing_event", (int)fe.subheader, (int)fe.info, (int)fe.dir); } for (const auto &de : w.drain_dungeon_events()) { emit_signal("dungeon_event", (int)de.subheader, (int)de.x, (int)de.y, de.has_destination); } for (const auto &oe : w.drain_observer_events()) { fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(oe.x, oe.y, 0); emit_signal("observer_event", (int)oe.kind, (int)oe.vid, Vector2(g.x, g.z)); } for (const auto &ec : w.drain_effect_cues()) { emit_signal("effect_cue", (int)ec.vid, String::utf8(ec.file.c_str()), (int)ec.special); } for (const auto &fc : w.drain_fly_cues()) { emit_signal("fly_cue", (int)fc.type, (int)fc.start_vid, (int)fc.end_vid); } for (const auto &tc : w.drain_fly_target_cues()) { emit_signal("fly_targeting", (int)tc.shooter_vid, (int)tc.target_vid, Vector2((float)tc.x, (float)tc.y), tc.append); } for (const auto &d : w.drain_damage()) { emit_signal("damage", (int)d.vid, (int)d.amount, (int)d.flag); } for (uint32_t vid : w.drain_mount_changes()) { emit_signal("mount_changed", (int)vid); } if (w.take_target_dirty()) { emit_signal("target_info", (int)w.target_vid(), (int)w.target_hp_pct()); } if (w.skills_dirty()) { emit_signal("skills_changed"); } if (w.skill_group_dirty()) { emit_signal("skill_group_changed", (int)w.skill_group()); } for (int sk : w.drain_cooldown_ends()) { emit_signal("skill_cooldown_end", sk); } if (w.quickslots_dirty()) { emit_signal("quickslots_changed"); } for (const auto &ac : w.drain_affects()) { if (ac.added) { for (const auto &a : w.affects()) { if (a.type == ac.type) { emit_signal("affect_added", affect_dict(a)); break; } } } else { emit_signal("affect_removed", (int)ac.type); } } for (const auto &ie : w.drain_item_events()) { if (ie.kind == mtnet::ItemEvent::Get) { emit_signal("item_picked_up", (int)ie.vnum, (int)ie.count, String::utf8(ie.from.c_str())); } else { emit_signal("item_used", (int)ie.vnum); } } for (uint32_t leader : w.drain_party_invites()) { emit_signal("party_invite_ask", (int)leader); } for (const auto &ev : w.drain_server_commands()) { switch (ev.kind) { case mtnet::ServerCommandEvent::PartyRequestDenied: emit_signal("party_request_denied"); break; case mtnet::ServerCommandEvent::LoverLogin: case mtnet::ServerCommandEvent::LoverLogout: case mtnet::ServerCommandEvent::LoverNear: case mtnet::ServerCommandEvent::LoverFar: case mtnet::ServerCommandEvent::LoverDivorce: // LoverInfo 由 mutator 同步更新;lover_changed 信号在 // take_lover_dirty() 处统一发出,这里无需单独 emit。 break; case mtnet::ServerCommandEvent::SafeboxPasswordRequired: emit_signal("safebox_password_required"); break; case mtnet::ServerCommandEvent::SafeboxWrongPassword: emit_signal("safebox_wrong_password"); break; case mtnet::ServerCommandEvent::MallPasswordRequired: emit_signal("mall_password_required"); break; case mtnet::ServerCommandEvent::RefineSucceeded: emit_signal("refine_result", true); break; case mtnet::ServerCommandEvent::RefineFailed: emit_signal("refine_result", false); break; case mtnet::ServerCommandEvent::PrivateShopOpenRequested: emit_signal("private_shop_open_requested"); break; case mtnet::ServerCommandEvent::MyShopPrice: emit_signal("my_shop_price_list", (int)ev.value, (int)ev.value2); break; case mtnet::ServerCommandEvent::BlockModeChanged: emit_signal("block_mode_changed", (int)ev.value); break; case mtnet::ServerCommandEvent::ObserverModeChanged: emit_signal("observer_mode_changed", ev.value != 0); break; case mtnet::ServerCommandEvent::ObserverCountChanged: emit_signal("observer_count_changed", (int)ev.value); break; case mtnet::ServerCommandEvent::StoneDetected: emit_signal("stone_detected", (int)ev.value, (int)ev.value2, ev.value3); break; case mtnet::ServerCommandEvent::StaminaStarted: emit_signal("stamina_changed", (int)ev.value2, (int)ev.value, true); break; case mtnet::ServerCommandEvent::StaminaStopped: emit_signal("stamina_changed", (int)ev.value2, 0, false); break; case mtnet::ServerCommandEvent::MobileFlagChanged: emit_signal("mobile_flag_changed", ev.value != 0); break; case mtnet::ServerCommandEvent::MobileAuthRequired: emit_signal("mobile_auth_required"); break; case mtnet::ServerCommandEvent::ComboChanged: emit_signal("combo_changed", ev.value != 0); break; case mtnet::ServerCommandEvent::GiftAvailable: emit_signal("gift_available"); break; } } if (w.party_dirty()) { emit_signal("party_changed"); } bool shop_was_open = shop_open_seen; if (w.shop_dirty()) { shop_open_seen = w.shop_open(); if (shop_open_seen && !shop_was_open) { emit_signal("shop_opened", (int)w.shop_vid()); } else if (!shop_open_seen && shop_was_open) { emit_signal("shop_closed"); } } for (const auto &err : w.drain_shop_errors()) { emit_signal("shop_error", String::utf8(err.c_str())); } for (const auto &sc : w.drain_scripts()) { emit_signal("script_dialog", (int)sc.skin, String::utf8(sc.text.c_str())); } for (const auto &cc : w.drain_confirms()) { emit_signal("quest_confirm_ask", String::utf8(cc.msg.c_str()), (int)cc.timeout, (int)cc.request_pid); } for (uint16_t qi : w.drain_quest_changes()) { emit_signal("quest_info", (int)qi); } if (w.friends_dirty()) { emit_signal("friends_changed"); } for (const auto &name : w.drain_friend_invites()) { emit_signal("friend_invite_ask", String::utf8(name.c_str())); } if (w.take_lover_dirty()) { emit_signal("lover_changed", get_lover()); } if (w.exchange_dirty()) { emit_signal("exchange_changed"); } if (w.safebox_dirty()) { emit_signal("safebox_changed"); } if (w.mall_dirty()) { bool mall_now = w.mall_open(); if (mall_now && !mall_open_seen) { emit_signal("mall_opened", w.mall_size()); } mall_open_seen = mall_now; emit_signal("mall_changed"); } for (const auto &ev : w.drain_cube_events()) { using CE = mtnet::EntityStore::CubeEvent; switch (ev) { case CE::Opened: emit_signal("cube_opened", (int)w.cube().npc_vnum); break; case CE::Closed: emit_signal("cube_closed"); break; case CE::InfoChanged: emit_signal("cube_changed"); break; case CE::Success: { auto result = w.cube_last_success(); emit_signal("cube_result", (int)result.vnum, result.count, true); break; } case CE::Fail: emit_signal("cube_result", 0, 0, false); break; } } if (w.guild_dirty()) { emit_signal("guild_changed"); } if (w.guild_comments_dirty()) { emit_signal("guild_comments_changed"); } for (const auto &invite : w.drain_guild_invites()) { emit_signal("guild_invite_ask", (int64_t)invite.guild_id, String::utf8(invite.guild_name.c_str())); } if (w.guild_skill_dirty()) { emit_signal("guild_skill_changed"); } if (w.guild_war_dirty()) { emit_signal("guild_war_changed"); } for (const auto &ev : w.drain_guild_war_events()) { emit_signal("guild_war_event", (int)ev.state, (int64_t)ev.opp_guild_id, String::utf8(w.guild_name(ev.opp_guild_id).c_str())); } for (const auto &score : w.drain_guild_war_scores()) { emit_signal("guild_war_point", (int64_t)score.gain_guild_id, (int64_t)score.opp_guild_id, score.point); } for (const auto &rc : w.drain_refine_cues()) { Dictionary d; d["type"] = (int)rc.type; d["pos"] = (int)rc.pos; d["src_vnum"] = (int)rc.src_vnum; d["result_vnum"] = (int)rc.result_vnum; d["cost"] = rc.cost; d["prob"] = rc.prob; Array mats; for (int i = 0; i < rc.material_count && i < 5; ++i) { if (rc.materials[i].vnum == 0) { continue; } Dictionary mat; mat["vnum"] = (int)rc.materials[i].vnum; mat["count"] = rc.materials[i].count; mats.push_back(mat); } d["materials"] = mats; emit_signal("refine_ask", d); } for (const auto &dc : w.drain_ds_cues()) { if (dc.sub_type == mtnet::DS_SUB_OPEN) { emit_signal("ds_window_open"); } else if (dc.sub_type == mtnet::DS_SUB_REFINE_SUCCEED) { emit_signal("ds_refine_result", true, (int)dc.sub_type, (int)dc.cell); } else if (dc.sub_type >= mtnet::DS_SUB_REFINE_FAIL && dc.sub_type <= mtnet::DS_SUB_REFINE_FAIL_TOO_MUCH_MATERIAL) { emit_signal("ds_refine_result", false, (int)dc.sub_type, (int)dc.cell); } } for (const auto &warp : w.drain_warps()) { fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(warp.x, warp.y, 0); // ClientVS22's RecvWarpPacket() always calls Connect(lAddr, wPort), // even for an address that points back to the current channel. Clear // the old loading/world state before the socket is replaced. w.reset_for_map_change(); emit_signal("world_reset"); emit_signal("warp", Vector3(g.x, g.y, g.z), false); uint32_t addr = static_cast(warp.addr); char host[16]; std::snprintf(host, sizeof(host), "%u.%u.%u.%u", addr & 0xFF, (addr >> 8) & 0xFF, (addr >> 16) & 0xFF, (addr >> 24) & 0xFF); if (!classic_sess->connect_warp(std::string(host), warp.port)) { // connect_warp() marks the session Failed. Let the normal stage // bridge surface it as a post-login disconnect on the next pump. return; } classic_last_stage = -1; shop_open_seen = false; mall_open_seen = false; dead_seen.clear(); set_stage(Stage::GameConnect); return; } if (w.take_time_dirty()) { emit_signal("time_changed", (int64_t)w.server_time()); } if (w.take_channel_dirty()) { emit_signal("channel_changed", w.channel()); } if (w.take_npc_marks_dirty()) { emit_signal("npc_marks_changed"); } if (w.take_land_dirty()) { emit_signal("land_areas_changed"); } if (w.take_markers_dirty()) { emit_signal("world_markers_changed"); } } } void M2Client::pump_auth() { auth->process(); if ((int)auth->state() != last_auth_state) { last_auth_state = (int)auth->state(); if (auth->state() == mtnet::NetStream::State::Online && stage == Stage::AuthConnect) { set_stage(Stage::AuthWait); } } if (!auth->done()) { return; } if (auth->success()) { uint32_t key = auth->login_key(); last_login_key = key; emit_signal("auth_ok", (int64_t)key); auth->disconnect(); auth.reset(); game = std::make_unique(std::string(account_id.utf8().get_data()), key); game->set_auto_enter_game(false); loading_since_ms = 0; enter_game_sent = false; if (!game->connect(std::string(game_host.utf8().get_data()), (uint16_t)game_port)) { emit_signal("login_failed", String(game->last_error().c_str())); set_stage(Stage::Failed); game.reset(); return; } set_stage(Stage::GameConnect); } else { emit_signal("login_failed", String(auth->fail_reason().empty() ? auth->last_error().c_str() : auth->fail_reason().c_str())); set_stage(Stage::Failed); auth.reset(); } } void M2Client::pump_game() { game->process(); if (game->empire_seen() && (int)game->empire() != last_game_empire) { last_game_empire = game->empire(); emit_signal("empire_changed", (int)game->empire()); } if ((int)game->phase() != last_game_phase) { last_game_phase = (int)game->phase(); emit_signal("phase_changed", String(phase_name(last_game_phase))); if (game->phase() != mtnet::PHASE_LOADING) { loading_since_ms = 0; } if (game->phase() == mtnet::PHASE_LOGIN && stage == Stage::GameConnect) { set_stage(Stage::GameLogin); } } if (game->char_list_ready() && !char_list_emitted) { char_list_emitted = true; emit_signal("char_list", build_char_list()); } // create / delete results (server patches the slot list, doesn't re-send it) for (const auto &ev : game->drain_char_events()) { switch (ev.kind) { case mtnet::GameClient::CharEvent::CreateOk: emit_signal("char_list", build_char_list()); emit_signal("char_created", ev.slot); break; case mtnet::GameClient::CharEvent::CreateFail: emit_signal("char_create_failed", ev.fail_type); break; case mtnet::GameClient::CharEvent::DeleteOk: emit_signal("char_list", build_char_list()); emit_signal("char_deleted", ev.slot); break; case mtnet::GameClient::CharEvent::DeleteFail: emit_signal("char_delete_failed"); break; } } for (const auto &ev : game->drain_name_events()) { emit_signal("char_list", build_char_list()); emit_signal("char_name_changed", (int64_t)ev.pid, String::utf8(ev.name.c_str())); } for (int i = 0, n = game->drain_guild_make_requests(); i < n; ++i) { emit_signal("guild_make_requested"); } if (game->failed()) { emit_signal("login_failed", String(game->fail_reason().c_str())); set_stage(Stage::Failed); game->disconnect(); game.reset(); return; } if (game->phase() == mtnet::PHASE_LOADING && !enter_game_sent) { uint64_t now = godot::Time::get_singleton()->get_ticks_msec(); if (loading_since_ms == 0) { loading_since_ms = now; } else if (now - loading_since_ms > 1500) { game->send_enter_game(); enter_game_sent = true; } } if (game->in_game() && stage != Stage::InGame) { set_stage(Stage::InGame); emit_signal("entered_game"); } // Advance the networked-world model and surface its deltas as signals. if (game->in_game()) { mtnet::EntityStore &w = game->world(); w.set_now((uint32_t)Time::get_singleton()->get_ticks_msec()); w.tick(); for (const auto &c : w.drain_changes()) { switch (c.kind) { case mtnet::EntityStore::ChangeKind::Spawn: if (const mtnet::Entity *e = w.get(c.vid)) { emit_signal("entity_spawned", entity_dict(*e)); } break; case mtnet::EntityStore::ChangeKind::Despawn: dead_seen.erase(c.vid); emit_signal("entity_despawned", (int)c.vid); break; case mtnet::EntityStore::ChangeKind::Move: emit_signal("entity_moved", (int)c.vid); break; case mtnet::EntityStore::ChangeKind::MainSet: emit_signal("entity_main_set", (int)c.vid); break; case mtnet::EntityStore::ChangeKind::Info: if (const mtnet::Entity *e = w.get(c.vid)) { emit_signal("entity_info", (int)c.vid, entity_dict(*e)); } break; } } // §2.5: bare field updates (ownership / affect flags) with no Spawn/Move/ // Info change — re-mirror the node so presentation stays in sync. for (uint32_t vid : w.drain_dirty()) { if (const mtnet::Entity *e = w.get(vid)) { emit_signal("entity_info", (int)vid, entity_dict(*e)); } } // §9.1: map background music. if (w.take_bgm_dirty()) { emit_signal("bgm_changed", String::utf8(w.bgm_name().c_str()), w.bgm_volume()); } for (const auto &m : w.drain_chat()) { if (m.type == mtnet::CHAT_TYPE_WHISPER) { emit_signal("whisper_received", (int)m.sub, String::utf8(m.from.c_str()), String::utf8(m.text.c_str())); } else { emit_signal("chat", (int)m.type, (int)m.vid, String::utf8(m.text.c_str())); } } for (uint32_t vid : w.drain_vitals()) { emit_signal("vitals_changed", (int)vid); const mtnet::Entity *e = w.get(vid); if (e && e->dead && !dead_seen.count(vid)) { dead_seen.insert(vid); emit_signal("entity_dead", (int)vid); } } for (const auto &d : w.drain_damage()) { emit_signal("damage", (int)d.vid, (int)d.amount, (int)d.flag); } for (const auto &mo : w.drain_motions()) { emit_signal("motion", (int)mo.vid, (int)mo.victim_vid, (int)mo.motion); } for (const auto &dm : w.drain_dig_motions()) { emit_signal("dig_motion", (int)dm.vid, (int)dm.target_vid, (int)dm.count); } for (const auto &fe : w.drain_fishing_events()) { emit_signal("fishing_event", (int)fe.subheader, (int)fe.info, (int)fe.dir); } for (const auto &de : w.drain_dungeon_events()) { emit_signal("dungeon_event", (int)de.subheader, (int)de.x, (int)de.y, de.has_destination); } for (const auto &oe : w.drain_observer_events()) { fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(oe.x, oe.y, 0); emit_signal("observer_event", (int)oe.kind, (int)oe.vid, Vector2(g.x, g.z)); } for (const auto &ec : w.drain_effect_cues()) { emit_signal("effect_cue", (int)ec.vid, String::utf8(ec.file.c_str()), (int)ec.special); } for (const auto &fc : w.drain_fly_cues()) { emit_signal("fly_cue", (int)fc.type, (int)fc.start_vid, (int)fc.end_vid); } for (const auto &tc : w.drain_fly_target_cues()) { emit_signal("fly_targeting", (int)tc.shooter_vid, (int)tc.target_vid, Vector2((float)tc.x, (float)tc.y), tc.append); } if (w.skills_dirty()) { emit_signal("skills_changed"); } if (w.skill_group_dirty()) { emit_signal("skill_group_changed", (int)w.skill_group()); } for (int sk : w.drain_cooldown_ends()) { emit_signal("skill_cooldown_end", sk); } if (w.quickslots_dirty()) { emit_signal("quickslots_changed"); } for (const auto &sc : w.drain_scripts()) { emit_signal("script_dialog", (int)sc.skin, String::utf8(sc.text.c_str())); } for (const auto &cc : w.drain_confirms()) { emit_signal("quest_confirm_ask", String::utf8(cc.msg.c_str()), (int)cc.timeout, (int)cc.request_pid); } for (uint16_t qi : w.drain_quest_changes()) { emit_signal("quest_info", (int)qi); } for (const auto &ac : w.drain_affects()) { if (ac.added) { for (const auto &a : w.affects()) { if (a.type == ac.type) { emit_signal("affect_added", affect_dict(a)); break; } } } else { emit_signal("affect_removed", (int)ac.type); } } if (w.take_points_dirty()) { emit_signal("points_changed", get_points()); } if (w.take_target_dirty()) { emit_signal("target_info", (int)w.target_vid(), (int)w.target_hp_pct()); } for (const auto &ic : w.drain_inv()) { emit_signal("inventory_changed", (int)ic.window, (int)ic.cell); } for (const auto &gc : w.drain_ground()) { if (gc.added) { const mtnet::GroundItem *g = w.ground(gc.vid); if (g) { Dictionary d; d["vid"] = (int)g->vid; d["vnum"] = (int)g->vnum; d["owner"] = String::utf8(g->owner.c_str()); fmt::m2coord::Vec3 gp = fmt::m2coord::position_to_godot(g->x, g->y, g->z); d["pos"] = Vector3(gp.x, gp.y, gp.z); emit_signal("ground_item_added", d); } } else { emit_signal("ground_item_removed", (int)gc.vid); } } for (uint32_t vid : w.drain_view_equipment_changes()) { emit_signal("view_equipment", (int)vid); } for (const auto &p : w.drain_pvp_changes()) { emit_signal("pvp_changed", (int)p.src_vid, (int)p.dst_vid, (int)p.mode); } if (w.take_duel_started()) { emit_signal("duel_changed", get_duel()); emit_signal("duel_started"); } for (const auto &ie : w.drain_item_events()) { if (ie.kind == mtnet::ItemEvent::Get) { emit_signal("item_picked_up", (int)ie.vnum, (int)ie.count, String::utf8(ie.from.c_str())); } else { emit_signal("item_used", (int)ie.vnum); } } // P8: social / shop / storage for (uint32_t leader_pid : w.drain_party_invites()) { emit_signal("party_invite_ask", (int)leader_pid); } for (const auto &ev : w.drain_server_commands()) { switch (ev.kind) { case mtnet::ServerCommandEvent::PartyRequestDenied: emit_signal("party_request_denied"); break; case mtnet::ServerCommandEvent::LoverLogin: case mtnet::ServerCommandEvent::LoverLogout: case mtnet::ServerCommandEvent::LoverNear: case mtnet::ServerCommandEvent::LoverFar: case mtnet::ServerCommandEvent::LoverDivorce: // LoverInfo 由 mutator 同步更新;lover_changed 信号在 // take_lover_dirty() 处统一发出,这里无需单独 emit。 break; case mtnet::ServerCommandEvent::SafeboxPasswordRequired: emit_signal("safebox_password_required"); break; case mtnet::ServerCommandEvent::SafeboxWrongPassword: emit_signal("safebox_wrong_password"); break; case mtnet::ServerCommandEvent::MallPasswordRequired: emit_signal("mall_password_required"); break; case mtnet::ServerCommandEvent::RefineSucceeded: emit_signal("refine_result", true); break; case mtnet::ServerCommandEvent::RefineFailed: emit_signal("refine_result", false); break; case mtnet::ServerCommandEvent::PrivateShopOpenRequested: emit_signal("private_shop_open_requested"); break; case mtnet::ServerCommandEvent::MyShopPrice: emit_signal("my_shop_price_list", (int)ev.value, (int)ev.value2); break; case mtnet::ServerCommandEvent::BlockModeChanged: emit_signal("block_mode_changed", (int)ev.value); break; case mtnet::ServerCommandEvent::ObserverModeChanged: emit_signal("observer_mode_changed", ev.value != 0); break; case mtnet::ServerCommandEvent::ObserverCountChanged: emit_signal("observer_count_changed", (int)ev.value); break; case mtnet::ServerCommandEvent::StoneDetected: emit_signal("stone_detected", (int)ev.value, (int)ev.value2, ev.value3); break; case mtnet::ServerCommandEvent::StaminaStarted: emit_signal("stamina_changed", (int)ev.value2, (int)ev.value, true); break; case mtnet::ServerCommandEvent::StaminaStopped: emit_signal("stamina_changed", (int)ev.value2, 0, false); break; case mtnet::ServerCommandEvent::MobileFlagChanged: emit_signal("mobile_flag_changed", ev.value != 0); break; case mtnet::ServerCommandEvent::MobileAuthRequired: emit_signal("mobile_auth_required"); break; case mtnet::ServerCommandEvent::ComboChanged: emit_signal("combo_changed", ev.value != 0); break; case mtnet::ServerCommandEvent::GiftAvailable: emit_signal("gift_available"); break; } } if (w.party_dirty()) { emit_signal("party_changed"); } if (w.take_lover_dirty()) { emit_signal("lover_changed", get_lover()); } if (w.friends_dirty()) { emit_signal("friends_changed"); } for (const auto &name : w.drain_friend_invites()) { emit_signal("friend_invite_ask", String::utf8(name.c_str())); } bool shop_was_open = shop_open_seen; if (w.shop_dirty()) { shop_open_seen = w.shop_open(); if (shop_open_seen && !shop_was_open) { emit_signal("shop_opened", (int)w.shop_vid()); } else if (!shop_open_seen && shop_was_open) { emit_signal("shop_closed"); } } for (const auto &err : w.drain_shop_errors()) { emit_signal("shop_error", String::utf8(err.c_str())); } if (w.exchange_dirty()) { emit_signal("exchange_changed"); } if (w.safebox_dirty()) { emit_signal("safebox_changed"); } if (w.mall_dirty()) { bool mall_now = w.mall_open(); if (mall_now && !mall_open_seen) { emit_signal("mall_opened", w.mall_size()); } mall_open_seen = mall_now; emit_signal("mall_changed"); } for (const auto &ev : w.drain_cube_events()) { using CE = mtnet::EntityStore::CubeEvent; switch (ev) { case CE::Opened: emit_signal("cube_opened", (int)w.cube().npc_vnum); break; case CE::Closed: emit_signal("cube_closed"); break; case CE::InfoChanged: emit_signal("cube_changed"); break; case CE::Success: { auto s = w.cube_last_success(); emit_signal("cube_result", (int)s.vnum, s.count, true); break; } case CE::Fail: emit_signal("cube_result", 0, 0, false); break; } } if (w.guild_dirty()) { emit_signal("guild_changed"); } if (w.guild_comments_dirty()) { emit_signal("guild_comments_changed"); } for (const auto &invite : w.drain_guild_invites()) { emit_signal("guild_invite_ask", (int64_t)invite.guild_id, String::utf8(invite.guild_name.c_str())); } if (w.guild_skill_dirty()) { emit_signal("guild_skill_changed"); } if (w.guild_war_dirty()) { emit_signal("guild_war_changed"); } for (const auto &ev : w.drain_guild_war_events()) { emit_signal("guild_war_event", (int)ev.state, (int64_t)ev.opp_guild_id, String::utf8(w.guild_name(ev.opp_guild_id).c_str())); } for (const auto &sc : w.drain_guild_war_scores()) { emit_signal("guild_war_point", (int64_t)sc.gain_guild_id, (int64_t)sc.opp_guild_id, sc.point); } for (const auto &mu : game->drain_mark_updates()) { emit_signal("guild_mark_updated", (int)mu.guild_id, (int)mu.img_idx); } for (const auto &rc : w.drain_refine_cues()) { Dictionary d; d["type"] = (int)rc.type; d["pos"] = (int)rc.pos; d["src_vnum"] = (int)rc.src_vnum; d["result_vnum"] = (int)rc.result_vnum; d["cost"] = rc.cost; d["prob"] = rc.prob; Array mats; for (int i = 0; i < rc.material_count && i < 5; ++i) { if (rc.materials[i].vnum == 0) { continue; } Dictionary m; m["vnum"] = (int)rc.materials[i].vnum; m["count"] = rc.materials[i].count; mats.push_back(m); } d["materials"] = mats; emit_signal("refine_ask", d); } for (const auto &dc : w.drain_ds_cues()) { if (dc.sub_type == mtnet::DS_SUB_OPEN) { emit_signal("ds_window_open"); } else if (dc.sub_type == mtnet::DS_SUB_REFINE_SUCCEED) { emit_signal("ds_refine_result", true, (int)dc.sub_type, (int)dc.cell); } else if (dc.sub_type >= mtnet::DS_SUB_REFINE_FAIL && dc.sub_type <= mtnet::DS_SUB_REFINE_FAIL_TOO_MUCH_MATERIAL) { emit_signal("ds_refine_result", false, (int)dc.sub_type, (int)dc.cell); } } // P9: world systems for (const auto &warp : w.drain_warps()) { fmt::m2coord::Vec3 g = fmt::m2coord::position_to_godot(warp.x, warp.y, 0); emit_signal("warp", Vector3(g.x, g.y, g.z), warp.same_server()); if (!warp.same_server()) { // int32 lAddr is a network-order IPv4; format dotted-quad. uint32_t a = (uint32_t)warp.addr; char host[16]; std::snprintf(host, sizeof(host), "%u.%u.%u.%u", a & 0xFF, (a >> 8) & 0xFF, (a >> 16) & 0xFF, (a >> 24) & 0xFF); warp_to_game_server(String(host), warp.port); return; // game client is being torn down; stop touching w } } if (w.take_time_dirty()) { emit_signal("time_changed", (int64_t)w.server_time()); } if (w.take_channel_dirty()) { emit_signal("channel_changed", w.channel()); } if (w.take_npc_marks_dirty()) { emit_signal("npc_marks_changed"); } if (w.take_land_dirty()) { emit_signal("land_areas_changed"); } if (w.take_markers_dirty()) { emit_signal("world_markers_changed"); } for (uint32_t vid : w.drain_mount_changes()) { emit_signal("mount_changed", (int)vid); } } if (game->state() == mtnet::NetStream::State::Offline && stage != Stage::Failed) { emit_signal("disconnected", String(game->last_error().c_str())); set_stage(Stage::Failed); game.reset(); } } } // namespace mtgodot