Files
mtgodot-poc/extension/src/net/m2_client.cpp
T

3611 lines
122 KiB
C++

#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 <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/os.hpp>
#include <godot_cpp/classes/image.hpp>
#include <godot_cpp/classes/time.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/array.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/vector2.hpp>
#include <godot_cpp/variant/vector3.hpp>
#include <m2_coord.h>
#include <cmath>
using namespace godot;
namespace mtgodot {
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("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("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("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);
// 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);
// 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_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("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")));
}
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();
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: direct connect to the game host,
// with the same post-login feature surface as the shared M2Client API.
if (OS::get_singleton()->get_environment("MT_PROTOCOL") == "classic") {
classic_sess = std::make_unique<mtnet::classic::ClassicSession>();
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(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<mtnet::AuthClient>(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::create_character(int slot, const godot::String &name, int job, int shape,
int con, int intel, int str, int dex) {
if (classic_sess) {
return classic_sess->create_character(slot, std::string(name.utf8().get_data()), job, shape,
con, intel, str, dex);
}
if (!game) {
return false;
}
return game->create_character(slot, std::string(name.utf8().get_data()), 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()) {
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()) {
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<mtnet::classic::SyncPosElement> classic_elements;
std::vector<mtnet::CGSyncPositionElement> 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 int64_t vid = (int64_t)d["vid"];
const int64_t x = (int64_t)d["x"];
const int64_t y = (int64_t)d["y"];
if (vid < 0 || vid > 0xffffffffLL || x < INT32_MIN || x > INT32_MAX ||
y < INT32_MIN || y > INT32_MAX) {
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()) {
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) {
if (!is_in_game()) {
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()) {
return false;
}
if (classic_sess) {
return classic_sess->send_chat((uint8_t)type, std::string(text.utf8().get_data()));
}
return game && game->send_chat((uint8_t)type, std::string(text.utf8().get_data()));
}
bool M2Client::whisper(const String &to, const String &text) {
if (classic_sess) {
return is_in_game() && classic_sess->send_whisper(std::string(to.utf8().get_data()),
std::string(text.utf8().get_data()));
}
if (!game || !is_in_game()) {
return false;
}
return game->send_whisper(std::string(to.utf8().get_data()),
std::string(text.utf8().get_data()));
}
bool M2Client::cast_skill(int motion_idx, double rot_deg, int x, int y) {
// FUNC_SKILL | (motion_idx & 0x7F); reuse move()'s rot/time handling.
return move(0x80 | (motion_idx & 0x7F), 0, rot_deg, x, y);
}
bool M2Client::use_skill(int skill_id, int target_vid) {
if (!is_in_game() || skill_id <= 0) {
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) {
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) {
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 item / 2 skill / 3 command / 4 emotion
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() || pos < 0 || pos >= mtnet::QUICKSLOT_MAX_NUM ||
type < 1 || type > 4 || ref < 0 || ref > 255) {
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() || pos < 0 || pos >= mtnet::QUICKSLOT_MAX_NUM) {
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() || pos < 0 || pos >= mtnet::QUICKSLOT_MAX_NUM ||
change_pos < 0 || change_pos >= mtnet::QUICKSLOT_MAX_NUM) {
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()) {
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()) {
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 (classic_sess) return is_in_game() && idx >= 0 && classic_sess->send_script_button((uint32_t)idx);
return game && is_in_game() && game->send_script_button((uint32_t)idx);
}
bool M2Client::script_select_item(int selection) {
if (!is_in_game() || selection < 0) {
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 &&
classic_sess->send_quest_confirm(yes, (uint32_t)request_pid);
return game && is_in_game() && request_pid >= 0 &&
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 && classic_sess->send_party_invite((uint32_t)vid);
return game && is_in_game() && 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 &&
classic_sess->send_party_answer((uint32_t)leader_pid, accept);
return game && is_in_game() && 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 && classic_sess->send_party_remove((uint32_t)pid);
return game && is_in_game() && pid >= 0 && game->send_party_remove((uint32_t)pid);
}
bool M2Client::party_use_skill(int skill_index, int target_vid) {
if (!is_in_game() || skill_index < 0 || skill_index > 255 || target_vid < 0) {
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() && mode >= 0 && mode <= 255 &&
classic_sess->send_party_parameter((uint8_t)mode);
return game && is_in_game() && game->send_party_parameter((uint8_t)mode);
}
bool M2Client::party_set_state(int pid, int role, bool on) {
if (!is_in_game() || pid <= 0 || role < 0 || role > 255) 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 <requester>`, 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;
return d;
}
bool M2Client::shop_buy(int pos, int count) {
if (classic_sess) return is_in_game() && pos >= 0 && pos <= 255 &&
classic_sess->send_shop_buy((uint8_t)pos, (uint8_t)(count < 1 ? 1 : count));
return game && is_in_game() && game->send_shop_buy((uint8_t)pos, (uint8_t)(count < 1 ? 1 : count));
}
bool M2Client::shop_sell(int inv_cell, int count) {
if (classic_sess) return is_in_game() && inv_cell >= 0 && inv_cell <= 255 &&
classic_sess->send_shop_sell((uint8_t)inv_cell, (uint8_t)(count < 1 ? 1 : count));
return game && is_in_game() &&
game->send_shop_sell((uint8_t)inv_cell, (uint8_t)(count < 1 ? 1 : 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 (classic_sess) {
return is_in_game() && classic_sess->send_exchange_start((uint32_t)vid);
}
return game && is_in_game() && game->send_exchange_start((uint32_t)vid);
}
bool M2Client::exchange_add_item(int inv_window, int inv_cell, int display_pos) {
if (classic_sess) {
return is_in_game() && classic_sess->send_exchange_item_add((uint8_t)inv_window,
(uint16_t)inv_cell, (uint8_t)display_pos);
}
return game && is_in_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 (gold < 0) {
return false;
}
if (classic_sess) {
return is_in_game() && classic_sess->send_exchange_gold((uint32_t)gold);
}
return game && is_in_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;
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;
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 (classic_sess) {
return is_in_game() && classic_sess->send_safebox_checkin((uint8_t)safe_pos,
(uint8_t)inv_window, (uint16_t)inv_cell);
}
return game && is_in_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 (classic_sess) {
return is_in_game() && classic_sess->send_safebox_checkout((uint8_t)safe_pos,
(uint8_t)inv_window, (uint16_t)inv_cell);
}
return game && is_in_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 (classic_sess) {
return is_in_game() && classic_sess->send_safebox_move((uint16_t)from_cell,
(uint16_t)to_cell, (uint8_t)(count < 1 ? 1 : count));
}
return game && is_in_game() &&
game->send_safebox_move((uint16_t)from_cell, (uint16_t)to_cell, (uint8_t)(count < 1 ? 1 : 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 (classic_sess) {
return is_in_game() && 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);
}
// --- private (PC) shop ---
bool M2Client::open_private_shop(const godot::String &sign, const godot::Array &items) {
if (!game && !classic_sess) {
return false;
}
if (classic_sess) {
std::vector<mtnet::classic::CGMyShopItem> v;
v.reserve(items.size());
for (int i = 0; i < items.size() && i < 39; ++i) {
Dictionary d = items[i];
mtnet::classic::CGMyShopItem e{};
e.vnum = (uint32_t)(int64_t)d.get("vnum", 0);
e.count = (uint8_t)(int)d.get("count", 1);
e.pos = {(uint8_t)(int)d.get("inv_window", mtnet::WINDOW_INVENTORY),
(uint16_t)(int)d.get("inv_cell", 0)};
e.price = (uint32_t)(int64_t)d.get("price", 0);
e.display_pos = (uint8_t)(int)d.get("display_pos", i);
v.push_back(e);
}
return classic_sess->send_private_shop(std::string(sign.utf8().get_data()), v);
}
std::vector<mtnet::MyShopItem> v;
for (int i = 0; i < items.size(); ++i) {
Dictionary d = items[i];
mtnet::MyShopItem e{};
e.vnum = (uint32_t)(int64_t)d.get("vnum", 0);
e.count = (uint8_t)(int)d.get("count", 1);
e.pos = {(uint8_t)(int)d.get("inv_window", mtnet::WINDOW_INVENTORY),
(uint16_t)(int)d.get("inv_cell", 0)};
e.price = (uint32_t)(int64_t)d.get("price", 0);
e.display_pos = (uint8_t)(int)d.get("display_pos", i);
v.push_back(e);
}
return game->send_open_private_shop(std::string(sign.utf8().get_data()), 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 (classic_sess) return is_in_game() && 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 (classic_sess) return is_in_game() && 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 (classic_sess) return is_in_game() && 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 (classic_sess) {
return is_in_game() && classic_sess->send_cube_add_item(cube_index, inventory_index);
}
if (!game || !is_in_game() || cube_index < 0 || cube_index >= 24 ||
inventory_index < 0 || inventory_index > 255) {
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 (classic_sess) return is_in_game() && classic_sess->send_cube_delete_item(cube_index);
return game && is_in_game() && cube_index >= 0 && cube_index < 24 &&
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 &&
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_ADD_MEMBER, (uint32_t)vid);
return game && is_in_game() && 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 &&
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_REMOVE_MEMBER, (uint32_t)pid);
return game && is_in_game() &&
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() && amount >= 0 &&
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_OFFER, (uint32_t)amount);
return game && is_in_game() && game->send_guild_sub_u32(mtnet::GUILD_CG_OFFER, (uint32_t)amount);
}
bool M2Client::guild_charge_gsp(int amount) {
if (amount < 0) 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 || authority < 0 || authority > 255) 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 || 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) 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) 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) 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 (classic_sess) return is_in_game() && skill_vnum >= 0 && target_vid >= 0 &&
classic_sess->send_guild_skill((uint32_t)skill_vnum, (uint32_t)target_vid);
return game && is_in_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 || port > 65535 || 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<mtnet::classic::ClassicMarkClient>(
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<mtnet::MarkClient>(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 || port > 65535 || host.is_empty() || guild_id <= 0) {
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<mtnet::classic::ClassicMarkClient>(
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<mtnet::MarkClient>(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<Image> &img) {
if ((!game && !classic_sess) || port <= 0 || port > 65535 || host.is_empty() || img.is_null()) {
return false;
}
Ref<Image> 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<mtnet::classic::ClassicMarkClient>(
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<mtnet::MarkClient>(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 || port > 65535 || host.is_empty() || file_bytes.is_empty()) {
return false;
}
std::vector<uint8_t> 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<mtnet::classic::ClassicMarkClient>(
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<mtnet::MarkClient>(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<Image> M2Client::get_guild_mark_image(int guild_id) const {
Ref<Image> img;
if (!mark_store) {
return img;
}
std::vector<uint32_t> 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<mtnet::MarkImageSet>(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<mtnet::MarkImageSet>(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<mtnet::MarkImageSet>(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<mtnet::MarkImageSet>(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 (classic_sess) return is_in_game() && pos >= 0 && pos < mtnet::INVENTORY_MAX_NUM &&
type >= 0 && type <= 255 && classic_sess->send_refine((uint8_t)pos, (uint8_t)type);
return game && is_in_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) n = mtnet::classic::DS_REFINE_GRID_MAX_NUM;
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) {
n = mtnet::DS_REFINE_WINDOW_MAX_NUM;
}
for (int i = 0; i < n; ++i) {
int cell = (int)cells[i];
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["mount_vnum"] = (int)e.mount_vnum;
d["guild"] = e.guild;
d["alignment"] = (int)e.alignment;
d["pk_mode"] = (int)e.pk_mode;
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["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()) {
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()) {
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()) {
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() || count < 1 || count > 255) 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()) return false;
if (classic_sess) {
if (sw < 0 || sw > 255 || tw < 0 || tw > 255 || sc < 0 || sc > 0xffff || tc < 0 || tc > 0xffff)
return false;
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 || count < 1 || count > 255) return false;
if (classic_sess) {
if (window < 0 || window > 255 || cell < 0 || cell > 0xffff) return false;
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()) {
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();
}
}
void M2Client::_notification(int what) {
switch (what) {
case NOTIFICATION_APPLICATION_PAUSED:
suspend();
break;
case NOTIFICATION_APPLICATION_RESUMED:
resume();
break;
default:
break;
}
}
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<mtnet::classic::ClassicSession>();
if (!classic_sess->connect(std::string(host.utf8().get_data()), (uint16_t)port,
std::string(cfg_id.utf8().get_data()), std::string(cfg_pw.utf8().get_data()))) {
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<mtnet::GameClient>(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();
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) {
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;
}
}
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::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);
emit_signal("warp", Vector3(g.x, g.y, g.z), warp.same_server());
if (!warp.same_server()) {
uint32_t addr = (uint32_t)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);
warp_to_game_server(String(host), warp.port);
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<mtnet::GameClient>(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;
}
}
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::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