#pragma once // EntityStore — headless model of the networked world: turns game-phase packets // (GC_MAIN_CHARACTER / GC_CHARACTER_ADD[2] / GC_CHARACTER_DEL / GC_MOVE / // GC_CHAT / GC_CHARACTER_UPDATE / GC_FLY_TARGETING) into entity state + // interpolated positions. // No Godot / engine dependency (unit-testable). A bridge layer polls // drain_changes() / drain_chat() and mirrors entities into Metin2World. // // Positions are Metin2 cm (server space). func mirrors CInstanceBase::FUNC_*. #include "wire.h" #include #include #include #include namespace mtnet { enum : uint8_t { FUNC_WAIT = 0, FUNC_MOVE = 1, FUNC_ATTACK = 2, FUNC_COMBO = 3, FUNC_MOB_SKILL = 4, FUNC_EMOTION = 5, FUNC_SKILL = 0x80, }; struct Entity { uint32_t vid = 0; uint16_t race = 0; uint8_t ch_type = 0; // CHRTYPE: 0 PC, 1 NPC, 2 MONSTER, 3 STONE, 4 WARP, ... std::string name; std::string shop_sign; // GC_SHOP_SIGN; empty when the private shop is closed uint16_t parts[CHR_EQUIPPART_NUM] = {0, 0, 0, 0}; bool is_main = false; float x = 0, y = 0, z = 0; // current (interpolated) position, cm float angle = 0; uint8_t func = FUNC_WAIT; uint8_t position = 0; uint8_t walk_mode = 0; // 0 walk, 1 run (server's WALKMODE_*) uint32_t fly_target_vid = 0; int32_t fly_target_x = 0, fly_target_y = 0; bool fly_target_set = false; uint16_t moving_speed = 0; bool moving = false; float sx = 0, sy = 0, tx = 0, ty = 0; uint32_t move_start_ms = 0; uint32_t move_dur_ms = 0; // combat / status (0 = unknown until the server sends it) uint8_t attack_speed = 0; // GC_CHARACTER_ADD[2]/UPDATE bAttackSpeed (x100) int32_t hp = 0, max_hp = 0; int32_t sp = 0, max_sp = 0; int32_t level = 0; bool dead = false; bool stunned = false; uint32_t mount_vnum = 0; // 0 = on foot (GC_MOUNT / GC_CHAR_ADD_INFO) int32_t guild = 0; int16_t alignment = 0; uint8_t pk_mode = 0; // --- W0 interface freeze (CLIENT-GAP §2.3/§2.5) --- uint8_t empire = 0; // 1 Shinsoo, 2 Chunjo, 3 Jinno (GC_CHAR_ADDITIONAL_INFO) uint64_t affect_flags = 0; // AFF_* bitset carried on the entity (buff icons / aura) uint32_t owner_vid = 0; // summon / horse owner; 0 = none uint32_t state_flags = 0; // presentation-layer latch bits (W2 defines the enum) }; // --- P9 world systems ---------------------------------------------------- // GC_WARP — teleport target. The m2dev protocol uses addr==0 as an in-place // warp; the 40250 classic protocol always reconnects, including when lAddr is // zero. `reconnect` records that protocol-specific distinction at parse time. struct WarpCue { int32_t x = 0, y = 0; int32_t addr = 0; uint16_t port = 0; bool reconnect = false; bool same_server() const { return !reconnect && addr == 0; } }; // One atlas/minimap NPC entry (GC_NPC_POSITION). struct NPCMark { uint8_t type = 0; uint32_t vnum = 0; std::string name; int32_t x = 0, y = 0; }; // A quest/world marker (GC_TARGET_CREATE/UPDATE/DELETE). struct WorldMarker { int32_t id = 0; std::string name; uint32_t vid = 0; uint8_t type = 0; // CREATE_TARGET_TYPE_* int32_t x = 0, y = 0; }; // One floating damage number (GC_DAMAGE_INFO). struct DamageEvent { uint32_t vid = 0; uint8_t flag = 0; // DAMAGE_* bits int32_t amount = 0; }; // A one-shot combat/emote motion (GC_MOTION). struct MotionEvent { uint32_t vid = 0; uint32_t victim_vid = 0; uint16_t motion = 0; }; // A mining animation broadcast (GC_DIG_MOTION). struct DigMotionEvent { uint32_t vid = 0; uint32_t target_vid = 0; uint8_t count = 0; }; struct FishingEvent { uint8_t subheader = 0; uint32_t info = 0; uint8_t dir = 0; }; struct DungeonEvent { uint8_t subheader = 0; int32_t x = 0; int32_t y = 0; bool has_destination = false; }; struct LandArea { uint32_t id = 0; int32_t x = 0, y = 0; int32_t width = 0, height = 0; uint32_t guild_id = 0; }; struct Observer { uint32_t vid = 0; int32_t x = 0, y = 0; // server centimetres }; struct ObserverEvent { enum Kind { Add, Remove, Move } kind = Add; uint32_t vid = 0; int32_t x = 0, y = 0; }; // A buff/debuff on the local player (GC_AFFECT_ADD/REMOVE). struct Affect { uint32_t type = 0; uint8_t point_idx = 0; int32_t value = 0; uint32_t flag = 0; int32_t duration = 0; }; struct AffectChange { uint32_t type = 0; bool added = false; }; // A one-shot special/specific effect to play on an entity (GC_SPECIAL/SPECIFIC_EFFECT). struct EffectCue { uint32_t vid = 0; int32_t special = -1; // >=0 for GC_SPECIAL_EFFECT (built-in id) std::string file; // set for GC_SPECIFIC_EFFECT (.mse path) }; // A projectile (GC_CREATE_FLY): type, from vid, to vid. struct FlyCue { uint8_t type = 0; uint32_t start_vid = 0; uint32_t end_vid = 0; }; // A server broadcast that sets or appends a shooter's fly target. struct FlyTargetCue { uint32_t shooter_vid = 0; uint32_t target_vid = 0; int32_t x = 0; int32_t y = 0; bool append = false; }; // An NPC dialog to render (GC_SCRIPT): skin + raw EventManager script text. struct ScriptCue { uint8_t skin = 0; std::string text; }; // A yes/no prompt (GC_QUEST_CONFIRM). struct ConfirmCue { std::string msg; int32_t timeout = 0; uint32_t request_pid = 0; }; // One quest-log entry (GC_QUEST_INFO, flag-driven). struct QuestInfo { uint16_t index = 0; uint8_t flag = 0; bool begin = false; std::string title; std::string clock_name; int32_t clock_value = 0; std::string counter_name; int32_t counter_value = 0; std::string icon; }; // One item in a slot (inventory / equipment). vnum 0 = empty. struct Item { uint32_t vnum = 0; uint8_t count = 0; uint32_t flags = 0; uint32_t anti_flags = 0; int32_t sockets[ITEM_SOCKET_SLOT_MAX_NUM] = {0, 0, 0}; ItemAttr attrs[ITEM_ATTRIBUTE_SLOT_MAX_NUM] = {}; bool empty() const { return vnum == 0; } }; // Snapshot supplied by GC_VIEW_EQUIP when inspecting another character. struct ViewedEquipment { uint32_t vid = 0; Item items[VIEW_EQUIP_WEAR_MAX_NUM] = {}; }; // --- P8 party (GC_PARTY_*) -------------------------------------------------- struct PartyMember { uint32_t pid = 0; uint32_t vid = 0; // 0 until GC_PARTY_LINK std::string name; uint8_t state = 0; // bit0 = leader (this fork's server) uint8_t hp_pct = 0; // 0..100 int16_t affects[PARTY_AFFECT_SLOT_MAX_NUM] = {0}; bool leader() const { return (state & 1) != 0; } }; // --- P8 messenger friend list (GC_MESSENGER) ------------------------------ struct Friend { std::string name; bool online = false; bool mobile = false; // 40250 GC_MESSENGER_MOBILE / connected-state bit }; // --- P8 NPC shop (GC_SHOP) ---------------------------------------------------- struct ShopEntry { uint32_t vnum = 0; uint32_t price = 0; uint8_t count = 0; uint8_t pos = 0; // slot index within its tab (0..SHOP_HOST_ITEM_MAX_NUM-1) }; // One shelf/tab of a SHOP_GC_START_EX shop. A plain SHOP_GC_START shop is // modelled as a single unnamed tab. struct ShopTab { std::string name; uint8_t coin_type = 0; std::vector items; }; // --- P8 exchange / trade (GC_EXCHANGE) ------------------------------------- struct ExchangeSlot { uint32_t vnum = 0; uint8_t count = 0; }; struct ExchangeState { bool active = false; uint32_t partner_vid = 0; ExchangeSlot self_items[12] = {}; ExchangeSlot peer_items[12] = {}; int64_t self_gold = 0; int64_t peer_gold = 0; bool self_accept = false; bool peer_accept = false; }; // --- guild (GC_GUILD) -------------------------------------------------------- struct GuildMember { uint32_t pid = 0; uint8_t grade = 0; bool is_general = false; bool online = false; uint8_t job = 0; uint8_t level = 0; uint32_t offer = 0; std::string name; }; struct GuildComment { uint32_t id = 0; std::string name; std::string content; }; struct GuildInvite { uint32_t guild_id = 0; std::string guild_name; }; struct GuildGrade { std::string name; uint8_t auth = 0; }; struct GuildState { bool in_guild = false; uint32_t id = 0; std::string name; uint8_t level = 0; uint32_t exp = 0; uint32_t gold = 0; uint16_t member_count = 0; uint16_t max_member_count = 0; uint32_t master_pid = 0; bool has_land = false; }; // GUILD_GC_SKILL_INFO — the guild-skill page. struct GuildSkillState { bool valid = false; uint8_t skill_point = 0; uint8_t levels[GUILD_SKILL_MAX_NUM] = {}; uint16_t guild_point = 0; uint16_t max_guild_point = 0; }; // GUILD_GC_WAR — our current guild-war status against one opponent. struct GuildWarStatus { uint32_t opp_guild_id = 0; uint8_t type = 0; uint8_t state = GUILD_WAR_NONE; }; // GUILD_GC_WAR_POINT — a scoreboard delta. struct GuildWarScore { uint32_t gain_guild_id = 0; uint32_t opp_guild_id = 0; int32_t point = 0; }; // --- dragon-soul refine (GC_DRAGON_SOUL_REFINE) -------------------------- struct DragonSoulCue { uint8_t sub_type = 0; // DS_SUB_* (OPEN / REFINE_SUCCEED / REFINE_FAIL_*) uint8_t window = 0; // affected item position (success/fail) uint16_t cell = 0; }; // --- refine (GC_REFINE_INFORMATION) ---------------------------------------- struct RefineCue { uint8_t type = 0; uint8_t pos = 0; // inventory cell of the item uint32_t src_vnum = 0; uint32_t result_vnum = 0; int32_t cost = 0; int32_t prob = 0; // success % struct Mat { uint32_t vnum = 0; int32_t count = 0; } materials[5] = {}; uint8_t material_count = 0; }; // One stateful line from the 40250 GC_CHAT/COMMAND bus. The original client // routes these through CPythonNetworkStream::ServerCommand(); keeping them as // typed events lets the Godot bridge expose the same behaviour without making // the parser depend on UI code. struct ServerCommandEvent { enum Kind { PartyRequestDenied, SafeboxPasswordRequired, SafeboxWrongPassword, MallPasswordRequired, RefineSucceeded, RefineFailed, PrivateShopOpenRequested, MyShopPrice, BlockModeChanged, ObserverModeChanged, ObserverCountChanged, StoneDetected, StaminaStarted, StaminaStopped, MobileFlagChanged, MobileAuthRequired, ComboChanged, GiftAvailable, }; Kind kind = PartyRequestDenied; uint32_t value = 0; // item vnum / price, depending on kind uint32_t value2 = 0; float value3 = 0.0f; // StoneDetect angle, in degrees after legacy conversion }; // An item lying in the world (GC_ITEM_GROUND_ADD). struct GroundItem { uint32_t vid = 0; uint32_t vnum = 0; float x = 0, y = 0, z = 0; // server cm std::string owner; }; // One inventory/equipment slot changed (window = WINDOW_INVENTORY / _EQUIPMENT). struct InvChange { uint8_t window = 0; uint16_t cell = 0; }; // A ground item appeared / vanished. struct GroundChange { uint32_t vid = 0; bool added = false; }; // "You picked up N x " (GC_ITEM_GET) or "someone used " (GC_ITEM_USE). struct ItemEvent { enum Kind { Get, Use }; Kind kind = Get; uint32_t vnum = 0; uint8_t count = 0; std::string from; // GC_ITEM_GET only }; struct PvpRelation { uint32_t src_vid = 0; uint32_t dst_vid = 0; uint8_t mode = 0; }; struct LoverInfo { std::string name; uint8_t love_point = 0; bool valid = false; }; // The local player's full stat array (GC_PLAYER_POINTS), indexed by EPointTypes. struct PlayerPoints { int32_t v[256] = {0}; int32_t hp() const { return v[POINT_HP]; } int32_t max_hp() const { return v[POINT_MAX_HP]; } int32_t sp() const { return v[POINT_SP]; } int32_t max_sp() const { return v[POINT_MAX_SP]; } int32_t stamina() const { return v[POINT_STAMINA]; } int32_t max_stamina() const { return v[POINT_MAX_STAMINA]; } int32_t level() const { return v[POINT_LEVEL]; } int32_t exp() const { return v[POINT_EXP]; } int32_t next_exp() const { return v[POINT_NEXT_EXP]; } int32_t gold() const { return v[POINT_GOLD]; } int32_t energy() const { return v[POINT_ENERGY]; } int32_t energy_end_time() const { return v[POINT_ENERGY_END_TIME]; } }; class EntityStore { public: enum class ChangeKind { Spawn, Despawn, Move, MainSet, Info }; struct Change { ChangeKind kind; uint32_t vid; }; struct ChatMsg { uint8_t type = 0; // EChatType; CHAT_TYPE_WHISPER for whispers uint32_t vid = 0; // speaker vid (0 for whisper / system) std::string text; std::string from; // whisper sender name (empty otherwise) uint8_t sub = 0; // whisper: WHISPER_TYPE_* }; void set_now(uint32_t now_ms) { m_now = now_ms; } // Feed one complete game-phase packet. `body` points at the packet start // (header/length included); `len` == that length. Unknown headers ignored. // This is the m2dev-fork parser; the 40250 "classic" backend calls the // protocol-neutral mutation API below instead (docs/CLIENT-40250-PORT.md §6). void apply(uint16_t header, const void *body, uint16_t len); // --- protocol-neutral mutation API --------------------------------------- // Both the m2dev apply() path and mtnet::classic::ClassicParser drive the // same world model through these. Positions are server cm; `rot256` is the // legacy 0..255 heading; `func` mirrors FUNC_*. void mut_spawn(uint32_t vid, uint16_t race, uint8_t ch_type, const std::string &name, float x, float y, float z, float angle, uint16_t moving_speed = 0, uint8_t attack_speed = 0); void mut_spawn_main(uint32_t vid, uint16_t race, const std::string &name, float x, float y, float z); // Full spawn (CLIENT-GAP §2.3): merge every field of `e` into the world in one // shot, keyed by e.vid, so a two-packet PC/NPC add (GC_CHARACTER_ADD + // GC_CHAR_ADDITIONAL_INFO) lands as a single Spawn change with name/parts/ // empire/affect_flags/owner already populated. Creates the entity if new. void mut_spawn_full(const Entity &e); // summon / horse ownership (CLIENT-GAP §2.5); no-op if vid unknown. void mut_ownership(uint32_t vid, uint32_t owner_vid); // Map background music (CLIENT-GAP §9.1). name is the resolved track id or // path; volume < 0 means "server did not specify, keep current". void mut_map_bgm(const std::string &name, float volume); void mut_despawn(uint32_t vid); // angle_deg is the already-decoded compass heading (m2dev: rot*360/256; // classic: rot*5 — the two wire encodings differ, so decode at the parser). void mut_move(uint32_t vid, float angle_deg, uint8_t func, float tx, float ty, uint32_t dur_ms); // Full stat block (index = POINT_*). `n` <= 256. void mut_set_points(const int32_t *pts, int n); // One stat delta (GC_CHARACTER_POINT_CHANGE equivalent). void mut_set_point(uint8_t type, int32_t value, uint32_t vid); // --- items --- window: 1=inventory, 2=equipment, 5=dragon-soul, 7=belt. // `sockets` is ITEM_SOCKET_SLOT_MAX_NUM ints; `attrs` is // ITEM_ATTRIBUTE_SLOT_MAX_NUM ItemAttr (pass nullptr to leave them zero). void mut_item_set(uint8_t window, uint16_t cell, uint32_t vnum, uint8_t count, uint32_t flags, uint32_t anti_flags, const int32_t *sockets, const ItemAttr *attrs); void mut_item_del(uint8_t window, uint16_t cell); void mut_item_update(uint8_t window, uint16_t cell, uint8_t count, const int32_t *sockets, const ItemAttr *attrs); void mut_ground_add(uint32_t vid, uint32_t vnum, float x, float y, float z); void mut_ground_del(uint32_t vid); void mut_ground_owner(uint32_t vid, const std::string &owner); void mut_item_used(uint32_t vnum); void mut_item_picked(uint32_t vnum, uint8_t count, const std::string &from); // --- entity state (all no-op if vid unknown) --- // `parts` is CHR_EQUIPPART_NUM u16 (nullptr = leave unchanged). void mut_char_update(uint32_t vid, const uint16_t *parts, uint8_t moving_speed, uint8_t attack_speed, int32_t guild_id, int16_t alignment, uint8_t pk_mode, uint32_t mount_vnum); void mut_set_position(uint32_t vid, uint8_t position); // snap an entity to (x,y) with no interpolation (GC_SYNC_POSITION). void mut_snap_position(uint32_t vid, float x, float y); void mut_change_speed(uint32_t vid, uint16_t moving_speed); void mut_walk_mode(uint32_t vid, uint8_t mode); void mut_stun(uint32_t vid); void mut_dead(uint32_t vid); void mut_motion(uint32_t vid, uint32_t victim_vid, uint16_t motion); void mut_target(uint32_t vid, uint8_t hp_pct); void mut_damage(uint32_t vid, uint8_t flag, int32_t amount); void mut_mount(uint32_t vid, uint32_t mount_vnum); void mut_fly(uint8_t type, uint32_t start_vid, uint32_t end_vid); void mut_fly_target(uint32_t shooter_vid, uint32_t target_vid, int32_t x, int32_t y, bool append); void mut_dig(uint32_t vid, uint32_t target_vid, uint8_t count); void mut_special_effect(uint32_t vid, int32_t special); void mut_specific_effect(uint32_t vid, const std::string &file); void mut_pvp(uint32_t src_vid, uint32_t dst_vid, uint8_t mode); void mut_duel_start(); void mut_duel_start(const uint32_t *opponents, int count); void mut_view_equipment(uint32_t vid, const Item *items, int n); // GC_CHAR_ADDITIONAL_INFO — fills a spawned entity's name/parts/level/guild. void mut_char_info(uint32_t vid, const std::string &name, const uint16_t *parts, uint8_t empire, int32_t guild_id, int32_t level, int16_t alignment, uint8_t pk_mode, uint32_t mount_vnum); // GC_SHOP_SIGN — the sign belongs to the entity that owns the private shop. void mut_shop_sign(uint32_t vid, const std::string &sign); // --- local-player skills / quickslots / affects --- // `level` / `master` are SKILL_MAX_NUM bytes (master may be nullptr). void mut_skill_levels(const uint8_t *level, const uint8_t *master, int n); void mut_skill_group(uint8_t group); void mut_skill_cooldown_end(uint8_t skill); void mut_quickslot_set(uint8_t pos, uint8_t type, uint8_t position); void mut_quickslot_del(uint8_t pos); void mut_quickslot_swap(uint8_t pos, uint8_t pos_to); void mut_affect_add(uint32_t type, uint8_t point_idx, int32_t value, uint32_t flag, int32_t duration); void mut_affect_remove(uint32_t type); // --- chat --- (GC_CHAT with type==CHAT_TYPE_COMMAND routes to apply_server_command) void mut_chat(uint8_t type, uint32_t vid, const std::string &text); void mut_whisper(uint8_t sub, const std::string &from, const std::string &text); // --- NPC shop (GC_SHOP) --- a single-shelf shop is one unnamed tab. void mut_shop_open(uint32_t vid, const std::vector &tabs); void mut_shop_update_item(uint8_t pos, uint32_t vnum, uint32_t price, uint8_t count); void mut_shop_update_price(int32_t price); void mut_shop_close(); void mut_shop_error(const std::string &code); // --- quest / NPC dialog --- void mut_script(uint8_t skin, const std::string &text); // GC_SCRIPT void mut_quest_confirm(const std::string &msg, int32_t timeout, uint32_t request_pid); void mut_quest_info(uint16_t index, uint8_t flag, bool begin, const std::string &title, const std::string &clock_name, int32_t clock_value, const std::string &counter_name, int32_t counter_value, const std::string &icon); // --- P9 world systems --- void mut_warp(int32_t x, int32_t y, int32_t addr, uint16_t port, bool reconnect = false); // Clear the loading-phase state that ClientVS22 resets before a new map // connection. Persistent account data (friends, guild, quests, inventory) // is retained; entities and map-local/transient state are discarded. void reset_for_map_change(); void mut_server_time(int64_t time); void mut_channel(uint8_t channel); void mut_npc_marks(const std::vector &marks); void mut_land_areas(const std::vector &areas); void mut_marker_create(int32_t id, const std::string &name, uint32_t vid, uint8_t type); void mut_marker_update(int32_t id, int32_t x, int32_t y); void mut_marker_delete(int32_t id); void mut_observer(ObserverEvent::Kind kind, uint32_t vid, int32_t x, int32_t y); void mut_lover(const std::string &name, uint8_t love_point); void mut_love_point(uint8_t love_point); void mut_fishing(uint8_t subheader, uint32_t info, uint8_t dir); void mut_dungeon(uint8_t subheader, int32_t x, int32_t y, bool has_destination); // --- party (GC_PARTY_*) --- `affects` is PARTY_AFFECT_SLOT_MAX_NUM int16 (may be nullptr). void mut_party_invite(uint32_t leader); void mut_party_add(uint32_t pid, const std::string &name); void mut_party_update(uint32_t pid, uint8_t role, uint8_t hp_pct, const int16_t *affects); void mut_party_remove(uint32_t pid); void mut_party_link(uint32_t pid, uint32_t vid); void mut_party_unlink(uint32_t pid); void mut_party_parameter(uint8_t distribute_mode); // --- messenger / exchange / storage ------------------------------------- void mut_friend_set(const std::string &name, bool online); void mut_friend_mobile(const std::string &name, bool mobile); void mut_friend_remove(const std::string &name); void mut_friend_clear(); // Server command `messenger_auth ` asks the local player to approve // or deny a pending friend request. void mut_friend_invite(const std::string &name); void mut_exchange_start(uint32_t partner_vid); void mut_exchange_item(bool self, uint8_t slot, uint32_t vnum, uint8_t count); void mut_exchange_item_del(bool self, uint8_t slot); void mut_exchange_gold(bool self, int64_t gold); void mut_exchange_accept(bool self, bool accepted); void mut_exchange_end(); void mut_exchange_notice(); // server-side ALREADY / LESS_GOLD notice void mut_safebox_open(int size); void mut_safebox_money(int64_t money); void mut_safebox_set(uint16_t cell, uint32_t vnum, uint8_t count, uint32_t flags, uint32_t anti_flags, const int32_t *sockets, const ItemAttr *attrs); void mut_safebox_del(uint16_t cell); void mut_safebox_wrong_password(); void mut_safebox_close(); void mut_mall_open(int size); void mut_mall_set(uint16_t cell, uint32_t vnum, uint8_t count, uint32_t flags, uint32_t anti_flags, const int32_t *sockets, const ItemAttr *attrs); void mut_mall_del(uint16_t cell); void mut_mall_close(); // Advance interpolation of moving entities to m_now. void tick(); const Entity *get(uint32_t vid) const; uint32_t main_vid() const { return m_main_vid; } std::vector vids() const; size_t size() const { return m_ents.size(); } // local player's full stat block; valid once GC_PLAYER_POINTS has arrived. const PlayerPoints &points() const { return m_points; } // local player's skill levels (index = skill id, 0..SKILL_MAX_NUM-1). uint8_t skill_level(int id) const { return (id >= 0 && id < SKILL_MAX_NUM) ? m_skills[id] : 0; } // 0 normal / 1 master / 2 grand master / 3 perfect master (GC_SKILL_LEVEL_NEW). uint8_t skill_master(int id) const { return (id >= 0 && id < SKILL_MAX_NUM) ? m_skill_master[id] : 0; } uint8_t skill_group() const { return m_skill_group; } bool skill_group_dirty() { bool d = m_skill_group_dirty; m_skill_group_dirty = false; return d; } bool skills_dirty() { bool d = m_skills_dirty; m_skills_dirty = false; return d; } // local player's quickslots (GC_QUICKSLOT_*), 0..QUICKSLOT_MAX_NUM-1. QuickSlot quickslot(int pos) const { return (pos >= 0 && pos < QUICKSLOT_MAX_NUM) ? m_quickslots[pos] : QuickSlot{}; } bool quickslots_dirty() { bool d = m_quickslots_dirty; m_quickslots_dirty = false; return d; } std::vector drain_cooldown_ends() { auto v = std::move(m_cooldown_ends); m_cooldown_ends.clear(); return v; } std::vector drain_fly_cues() { auto v = std::move(m_fly_cues); m_fly_cues.clear(); return v; } std::vector drain_fly_target_cues() { auto v = std::move(m_fly_target_cues); m_fly_target_cues.clear(); return v; } std::vector drain_scripts() { auto v = std::move(m_scripts); m_scripts.clear(); return v; } std::vector drain_confirms() { auto v = std::move(m_confirms); m_confirms.clear(); return v; } // vids of quest-log entries changed since last drain. std::vector drain_quest_changes() { auto v = std::move(m_quest_changes); m_quest_changes.clear(); return v; } const QuestInfo *quest(uint16_t index) const { auto it = m_quests.find(index); return it == m_quests.end() ? nullptr : &it->second; } std::vector quest_indices() const { std::vector v; v.reserve(m_quests.size()); for (auto &kv : m_quests) { v.push_back(kv.first); } return v; } // currently-selected target's HP percent (GC_TARGET_INFO); 0 vid = none. uint32_t target_vid() const { return m_target_vid; } uint8_t target_hp_pct() const { return m_target_hp_pct; } // --- P8 party --- bool in_party() const { return !m_party.empty(); } std::vector party_pids() const { std::vector v; v.reserve(m_party.size()); for (auto &kv : m_party) { v.push_back(kv.first); } return v; } const PartyMember *party_member(uint32_t pid) const { auto it = m_party.find(pid); return it == m_party.end() ? nullptr : &it->second; } uint8_t party_distribute_mode() const { return m_party_mode; } bool party_dirty() { bool d = m_party_dirty; m_party_dirty = false; return d; } std::vector drain_party_invites() { auto v = std::move(m_party_invites); m_party_invites.clear(); return v; } // --- P8 messenger --- std::vector friends() const { std::vector v; v.reserve(m_friends.size()); for (auto &kv : m_friends) { v.push_back(kv.second); } return v; } bool friends_dirty() { bool d = m_friends_dirty; m_friends_dirty = false; return d; } std::vector drain_friend_invites() { auto v = std::move(m_friend_invites); m_friend_invites.clear(); return v; } // --- P8 NPC shop --- bool shop_open() const { return m_shop_open; } uint32_t shop_vid() const { return m_shop_vid; } int32_t shop_selling_price() const { return m_shop_selling_price; } const std::vector &shop_items() const { return m_shop_items; } // SHOP_GC_START_EX shelves; empty for a plain SHOP_GC_START shop (use shop_items()). const std::vector &shop_tabs() const { return m_shop_tabs; } bool shop_dirty() { bool d = m_shop_dirty; m_shop_dirty = false; return d; } std::vector drain_shop_errors() { auto v = std::move(m_shop_errors); m_shop_errors.clear(); return v; } // --- P8 exchange --- const ExchangeState &exchange() const { return m_exchange; } bool exchange_dirty() { bool d = m_exchange_dirty; m_exchange_dirty = false; return d; } // --- P8 safebox --- bool safebox_open() const { return m_safebox_open; } int safebox_size() const { return m_safebox_size; } int64_t safebox_gold() const { return m_safebox_gold; } const Item &safebox_slot(int cell) const; bool safebox_dirty() { bool d = m_safebox_dirty; m_safebox_dirty = false; return d; } // --- item-mall (창고몰) --- bool mall_open() const { return m_mall_open; } int mall_size() const { return m_mall_size; } const Item &mall_slot(int cell) const; bool mall_dirty() { bool d = m_mall_dirty; m_mall_dirty = false; return d; } // --- cube (제작) --- struct CubeResultEntry { uint32_t vnum = 0; int count = 0; }; struct CubeMaterialSlot { uint32_t vnum = 0; int count = 0; }; struct CubeRecipe { // one craftable output + its materials uint32_t result_vnum = 0; int result_count = 0; int64_t gold = 0; std::vector> material_groups; // any-of groups }; struct CubeState { bool open = false; uint32_t npc_vnum = 0; int64_t need_gold = 0; // gold the current pending craft needs uint32_t need_item_vnum = 0; // last "cube info" hint int need_item_count = 0; std::vector results; // r_list: what this NPC can make std::vector recipes; // m_info: materials per result }; enum class CubeEvent { Opened, Closed, InfoChanged, Success, Fail }; const CubeState &cube() const { return m_cube; } std::vector drain_cube_events() { auto v = std::move(m_cube_events); m_cube_events.clear(); return v; } // last Success payload (valid right after a CubeEvent::Success is drained) CubeResultEntry cube_last_success() const { return m_cube_last_success; } // --- guild --- void mut_guild_info(uint16_t member_count, uint16_t max_member_count, uint32_t id, uint32_t master_pid, uint32_t exp, uint8_t level, const std::string &name, uint32_t gold, bool has_land); void mut_guild_clear_members(); void mut_guild_member(uint32_t pid, uint8_t grade, bool is_general, uint8_t job, uint8_t level, uint32_t offer, const std::string &name); void mut_guild_member_online(uint32_t pid, bool online); void mut_guild_remove_member(uint32_t pid); void mut_guild_grade(uint8_t grade, const std::string &name, uint8_t auth); void mut_guild_grade_name(uint8_t grade, const std::string &name); void mut_guild_grade_auth(uint8_t grade, uint8_t auth); void mut_guild_member_grade(uint32_t pid, uint8_t grade); void mut_guild_member_general(uint32_t pid, bool is_general); void mut_guild_exp(uint8_t level, uint32_t exp); void mut_guild_money(uint32_t gold); void mut_guild_skill(uint8_t skill_point, const uint8_t *levels, uint16_t guild_point, uint16_t max_guild_point); void mut_guild_war(uint32_t self_id, uint32_t opponent_id, uint8_t type, uint8_t state); void mut_guild_war_pairs(const std::vector &pairs, bool remove); void mut_guild_war_score(uint32_t gain_guild_id, uint32_t opponent_guild_id, int32_t point); void mut_guild_name(uint32_t id, const std::string &name); void mut_guild_comments(const std::vector &comments); void mut_guild_invite(uint32_t guild_id, const std::string &guild_name); const GuildState &guild() const { return m_guild; } std::vector guild_members() const { std::vector v; v.reserve(m_guild_members.size()); for (auto &kv : m_guild_members) { v.push_back(kv.second); } return v; } const GuildGrade &guild_grade(int i) const { static const GuildGrade kEmpty; return (i >= 0 && i < 16) ? m_guild_grades[i] : kEmpty; } bool guild_dirty() { bool d = m_guild_dirty; m_guild_dirty = false; return d; } const std::vector &guild_comments() const { return m_guild_comments; } bool guild_comments_dirty() { bool d = m_guild_comments_dirty; m_guild_comments_dirty = false; return d; } std::vector drain_guild_invites() { auto v = std::move(m_guild_invites); m_guild_invites.clear(); return v; } // --- guild war / guild skill --- const GuildSkillState &guild_skill() const { return m_guild_skill; } bool guild_skill_dirty() { bool d = m_guild_skill_dirty; m_guild_skill_dirty = false; return d; } const GuildWarStatus &guild_war() const { return m_guild_war; } // active GvG pairs (src,dst); order not significant. const std::vector &guild_wars() const { return m_guild_wars; } bool guild_war_dirty() { bool d = m_guild_war_dirty; m_guild_war_dirty = false; return d; } std::string guild_name(uint32_t id) const { auto it = m_guild_names.find(id); return it == m_guild_names.end() ? std::string() : it->second; } // one-shot WAR state transitions (declare/accept/start/end) for toasts. std::vector drain_guild_war_events() { auto v = std::move(m_guild_war_events); m_guild_war_events.clear(); return v; } std::vector drain_guild_war_scores() { auto v = std::move(m_guild_war_scores); m_guild_war_scores.clear(); return v; } // --- refine --- std::vector drain_refine_cues() { auto v = std::move(m_refine_cues); m_refine_cues.clear(); return v; } std::vector drain_ds_cues() { auto v = std::move(m_ds_cues); m_ds_cues.clear(); return v; } void mut_refine(uint8_t type, uint8_t pos, uint32_t src_vnum, uint32_t result_vnum, uint8_t material_count, int32_t cost, int32_t prob, const RefineCue::Mat *materials); void mut_dragon_soul_refine(uint8_t sub_type, uint8_t window, uint16_t cell); const Item &dragon_soul_slot(int cell) const; // --- P9 world systems --- std::vector drain_warps() { auto v = std::move(m_warps); m_warps.clear(); return v; } // server wall-clock (unix seconds) as of the last GC_TIME; add the elapsed // real time yourself for a running clock. int64_t server_time() const { return m_server_time; } bool take_time_dirty() { bool d = m_time_dirty; m_time_dirty = false; return d; } int channel() const { return m_channel; } bool take_channel_dirty() { bool d = m_channel_dirty; m_channel_dirty = false; return d; } const std::vector &npc_marks() const { return m_npc_marks; } bool take_npc_marks_dirty() { bool d = m_npc_marks_dirty; m_npc_marks_dirty = false; return d; } std::vector markers() const { std::vector v; v.reserve(m_markers.size()); for (auto &kv : m_markers) { v.push_back(kv.second); } return v; } bool take_markers_dirty() { bool d = m_markers_dirty; m_markers_dirty = false; return d; } std::vector drain_mount_changes() { auto v = std::move(m_mount_changes); m_mount_changes.clear(); return v; } // items: normal inventory, 24 wear positions, and the independent 4x4 belt inventory. const Item &inv_slot(int cell) const; const Item &equip_slot(int wear) const; const Item &belt_slot(int cell) const; const Item &item_slot(uint8_t window, int cell) const; const ViewedEquipment *viewed_equipment(uint32_t vid) const; const GroundItem *ground(uint32_t vid) const; std::vector ground_vids() const; std::vector drain_inv() { auto v = std::move(m_inv_changes); m_inv_changes.clear(); return v; } std::vector drain_ground() { auto v = std::move(m_ground_changes); m_ground_changes.clear(); return v; } std::vector drain_view_equipment_changes() { auto v = std::move(m_view_equipment_changes); m_view_equipment_changes.clear(); return v; } std::vector drain_item_events() { auto v = std::move(m_item_events); m_item_events.clear(); return v; } std::vector drain_pvp_changes() { auto v = std::move(m_pvp_changes); m_pvp_changes.clear(); return v; } std::vector pvp_relations() const; const std::vector &duel_opponents() const { return m_duel_opponents; } bool duel_cannot_attack() const { return m_duel_cannot_attack; } bool take_duel_started() { bool started = m_duel_started; m_duel_started = false; return started; } const LoverInfo &lover() const { return m_lover; } bool observer_mode() const { return m_observer_mode; } int observer_count() const { return m_observer_count; } bool mobile_flag() const { return m_mobile_flag; } bool combo_skill_flag() const { return m_combo_skill_flag; } bool stamina_consuming() const { return m_stamina_consuming; } uint32_t stamina_per_sec() const { return m_stamina_per_sec; } uint32_t current_stamina() const { return m_current_stamina; } bool take_lover_dirty() { bool dirty = m_lover_dirty; m_lover_dirty = false; return dirty; } std::vector drain_changes() { auto v = std::move(m_changes); m_changes.clear(); return v; } // vids whose entity fields changed without a Spawn/Move/Info change since the // last drain (deduped). W2 uses this for the §2.1 visibility pass so a bare // ownership/affect update still re-mirrors the node. std::vector drain_dirty() { auto v = std::move(m_dirty); m_dirty.clear(); return v; } // Map BGM (§9.1). Returns true once after mut_map_bgm and consumes the flag. bool take_bgm_dirty() { bool d = m_bgm_dirty; m_bgm_dirty = false; return d; } const std::string &bgm_name() const { return m_bgm_name; } float bgm_volume() const { return m_bgm_volume; } std::vector drain_chat() { auto v = std::move(m_chat); m_chat.clear(); return v; } std::vector drain_server_commands() { auto v = std::move(m_server_commands); m_server_commands.clear(); return v; } // vids whose hp/sp/level/dead/stunned changed since last drain (deduped). std::vector drain_vitals() { auto v = std::move(m_vitals); m_vitals.clear(); return v; } std::vector drain_damage() { auto v = std::move(m_damage); m_damage.clear(); return v; } std::vector drain_motions() { auto v = std::move(m_motions); m_motions.clear(); return v; } std::vector drain_dig_motions() { auto v = std::move(m_dig_motions); m_dig_motions.clear(); return v; } std::vector drain_fishing_events() { auto v = std::move(m_fishing_events); m_fishing_events.clear(); return v; } std::vector drain_dungeon_events() { auto v = std::move(m_dungeon_events); m_dungeon_events.clear(); return v; } const std::vector &land_areas() const { return m_land_areas; } bool take_land_dirty() { bool d = m_land_dirty; m_land_dirty = false; return d; } std::vector observers() const { std::vector v; v.reserve(m_observers.size()); for (auto &kv : m_observers) v.push_back(kv.second); return v; } std::vector drain_observer_events() { auto v = std::move(m_observer_events); m_observer_events.clear(); return v; } std::vector drain_affects() { auto v = std::move(m_affect_changes); m_affect_changes.clear(); return v; } std::vector drain_effect_cues() { auto v = std::move(m_effect_cues); m_effect_cues.clear(); return v; } std::vector affects() const { std::vector v; v.reserve(m_affects.size()); for (auto &kv : m_affects) { v.push_back(kv.second); } return v; } // true once since GC_PLAYER_POINTS last arrived (consumes the flag). bool take_points_dirty() { bool d = m_points_dirty; m_points_dirty = false; return d; } // true once since GC_TARGET_INFO last arrived (consumes the flag). bool take_target_dirty() { bool d = m_target_dirty; m_target_dirty = false; return d; } private: Entity &touch(uint32_t vid, bool &created); void start_move(Entity &e, float tx, float ty, uint32_t start_ms, uint32_t dur_ms, uint8_t func); void mark_vitals(uint32_t vid); Item *mut_slot(uint8_t window, uint16_t cell); std::unordered_map m_ents; uint32_t m_main_vid = 0; uint32_t m_now = 0; std::vector m_changes; std::vector m_dirty; std::string m_bgm_name; float m_bgm_volume = -1.0f; bool m_bgm_dirty = false; std::vector m_chat; std::vector m_vitals; std::vector m_damage; std::vector m_motions; std::vector m_dig_motions; std::vector m_fishing_events; std::vector m_dungeon_events; std::vector m_land_areas; bool m_land_dirty = false; std::unordered_map m_observers; std::vector m_observer_events; std::unordered_map m_affects; std::vector m_affect_changes; std::vector m_effect_cues; std::vector m_fly_cues; std::vector m_fly_target_cues; uint8_t m_skills[SKILL_MAX_NUM] = {0}; uint8_t m_skill_master[SKILL_MAX_NUM] = {0}; uint8_t m_skill_group = 0; bool m_skill_group_dirty = false; bool m_skills_dirty = false; QuickSlot m_quickslots[QUICKSLOT_MAX_NUM] = {}; bool m_quickslots_dirty = false; std::vector m_cooldown_ends; std::vector m_scripts; std::vector m_confirms; std::unordered_map m_quests; std::vector m_quest_changes; PlayerPoints m_points; bool m_points_dirty = false; uint32_t m_target_vid = 0; uint8_t m_target_hp_pct = 0; bool m_target_dirty = false; Item m_inventory[INVENTORY_MAX_NUM]; Item m_equipment[WEAR_MAX_NUM]; Item m_belt[BELT_INVENTORY_MAX_NUM]; std::unordered_map m_view_equipment; std::unordered_map m_ground; std::vector m_inv_changes; std::vector m_ground_changes; std::vector m_view_equipment_changes; std::vector m_item_events; std::unordered_map m_pvp; std::vector m_pvp_changes; std::vector m_duel_opponents; bool m_duel_started = false; bool m_duel_cannot_attack = false; LoverInfo m_lover; bool m_lover_dirty = false; bool m_observer_mode = false; int m_observer_count = 0; bool m_mobile_flag = false; bool m_combo_skill_flag = false; bool m_stamina_consuming = false; uint32_t m_stamina_per_sec = 0; uint32_t m_current_stamina = 0; float m_stamina_current_f = 0.0f; uint32_t m_stamina_last_ms = 0; // P8 social / shop / storage std::unordered_map m_party; uint8_t m_party_mode = 0; bool m_party_dirty = false; std::vector m_party_invites; std::unordered_map m_friends; bool m_friends_dirty = false; std::vector m_friend_invites; bool m_shop_open = false; uint32_t m_shop_vid = 0; std::vector m_shop_items; // == m_shop_tabs[0].items when tabs present std::vector m_shop_tabs; bool m_shop_dirty = false; int32_t m_shop_selling_price = 0; std::vector m_shop_errors; ExchangeState m_exchange; bool m_exchange_dirty = false; Item m_safebox[SAFEBOX_MAX_NUM]; bool m_safebox_open = false; int m_safebox_size = 0; int64_t m_safebox_gold = 0; bool m_safebox_dirty = false; Item m_mall[MALL_MAX_NUM]; bool m_mall_open = false; int m_mall_size = 0; bool m_mall_dirty = false; CubeState m_cube; std::vector m_cube_events; CubeResultEntry m_cube_last_success; std::vector m_server_commands; void apply_server_command(const std::string &line); // P9 world systems std::vector m_warps; int64_t m_server_time = 0; bool m_time_dirty = false; int m_channel = 0; bool m_channel_dirty = false; std::vector m_npc_marks; bool m_npc_marks_dirty = false; std::unordered_map m_markers; bool m_markers_dirty = false; std::vector m_mount_changes; // guild / refine GuildState m_guild; std::unordered_map m_guild_members; std::vector m_guild_comments; std::vector m_guild_invites; GuildGrade m_guild_grades[16]; bool m_guild_dirty = false; bool m_guild_comments_dirty = false; GuildSkillState m_guild_skill; bool m_guild_skill_dirty = false; GuildWarStatus m_guild_war; std::vector m_guild_wars; bool m_guild_war_dirty = false; std::unordered_map m_guild_names; std::vector m_guild_war_events; std::vector m_guild_war_scores; std::vector m_refine_cues; // dragon-soul refine Item m_dragon_soul[DRAGON_SOUL_MAX_NUM]; std::vector m_ds_cues; }; } // namespace mtnet