Files
mtgodot-poc/extension/src/net/entity_store.h
T
shenleiandClaude Code 8092ff44e2 装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排,
  新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS),
  帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案
- 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、
  物品槽失配置灰(服务器回收/换武器后刷新)
- 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、
  被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1)
- 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/
  near/far/divorce 命令事件→在线/在身边状态与离婚隐藏
- 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示;
  仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close;
  仓库金钱经核实 40250 无 wire 协议,UI 定界只读
- app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫
- 相机 look_at 零向量守卫(首帧/传送落点)
- 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为
  HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路)
- docs/CLIENT-GAP.md 同步增量 129

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
2026-09-07 21:43:30 +09:00

1319 lines
46 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <cstdint>
#include <deque>
#include <string>
#include <unordered_map>
#include <vector>
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,
};
// CLIENT-GAP §3.2: one entry of a remote actor's TCP state queue, mirroring the
// reference CInstanceBase::PushTCPState / StateProcess model. `chk_time` is the
// server command time (TPacketGCMove::dwTime); the entry is released once the
// store's server-frame clock reaches it. `arg` carries the motion index for the
// FUNC_COMBO / FUNC_SKILL cases.
struct StateCmd {
uint32_t chk_time = 0;
float dst_x = 0, dst_y = 0;
float rot = 0;
uint8_t func = FUNC_WAIT;
uint8_t arg = 0;
uint32_t dur_ms = 0; // server-supplied walk duration; 0 = derive from distance/speed
};
// Staleness valve for the server-frame gate. The reference StateProcess has no
// such cap, but our server-frame clock is not yet validated against a live
// 40250 server (§3.2): if chk_time is stamped in a timebase we don't share, a
// naive "wait until frame >= chk_time" would freeze the remote actor forever.
// A chk_time more than this far ahead of the store's frame clock is treated as
// out-of-band and released immediately. Deliberate deviation, documented in
// CLIENT-GAP-FIX.md §3.2.
static constexpr uint32_t STATE_QUEUE_MAX_WAIT_MS = 1000;
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;
// CLIENT-GAP §3.2: TCP state queue + the "do this once the walk reaches Dst"
// latch (reference m_kMovAfterFunc / m_dwMovAfterArg). `skip_collision` mirrors
// __EnableSkipCollision(): a walk kicked off by a network state packet ignores
// actor-vs-actor collision until it arrives.
std::deque<StateCmd> state_queue;
uint8_t mov_after_func = FUNC_WAIT;
uint8_t mov_after_arg = 0;
float mov_after_rot = 0;
bool skip_collision = false;
// 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; // server AFFECT_STUN (InstanceBaseEffect.cpp:932)
bool knock_down = false; // knockback motion is playing (CActorInstance::IsKnockDown)
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<ShopEntry> items;
};
// --- P8 exchange / trade (GC_EXCHANGE) -------------------------------------
struct ExchangeSlot {
uint32_t vnum = 0;
uint8_t count = 0;
int32_t sockets[ITEM_SOCKET_SLOT_MAX_NUM] = {0, 0, 0};
ItemAttr attrs[ITEM_ATTRIBUTE_SLOT_MAX_NUM] = {};
};
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;
// EXCHANGE_GC_ALREADY / LESS_GOLD 的线上的子头值(0 = 无)。服务器在成功与
// 取消两种结局都发 GC_ENDexchange.cpp EXCHANGE_END 统一 Cancel),成败区分
// 只靠 CHAT_TYPE_INFO 文案;这里只承载这两个机器可判的附加码。
uint8_t last_notice = 0;
};
// --- 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,
LoverLogin,
LoverLogout,
LoverNear,
LoverFar,
LoverDivorce,
};
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 <vnum>" (GC_ITEM_GET) or "someone used <vnum>" (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;
bool online = false; // CHAT_TYPE_COMMAND "lover_login"/"lover_logout"marriage.cpp
bool near = false; // CHAT_TYPE_COMMAND "lover_near"/"lover_far"
};
// 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; }
// CLIENT-GAP §3.2: the server-frame clock the TCP state queue releases against
// (reference ELTimer_GetServerFrameMSec). 0 = "not clock-aligned yet" — while
// it is 0 every queued command is released immediately (degrades to the old
// apply-on-arrival behaviour until the handshake lDelta clock is wired, §3.2/5).
void set_server_frame_ms(uint32_t ms) { m_server_frame_ms = ms; }
uint32_t server_frame_ms() const { return m_server_frame_ms; }
// Release every due entry of every entity's TCP state queue. Called from tick()
// and again right after mut_move() enqueues, so an already-due command applies
// synchronously.
void process_states();
// 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);
// Affect bitset from GC_CHARACTER_ADD[2] / GC_CHARACTER_UPDATE, delivered as
// the two 32-bit wire words (CLIENT-GAP §2.5: v = lo | (hi << 32)). Queues a
// dirty tick so net_world re-mirrors the node; no-op if vid unknown.
void mut_affect_flags(uint32_t vid, uint32_t lo, uint32_t hi);
// 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).
// CLIENT-GAP §3.2: enqueues a StateCmd on the entity's TCP state queue rather
// than mutating the position directly, then drains anything already due.
// `arg` is the motion index (FUNC_COMBO / FUNC_SKILL); `chk_time` is the
// server command time (0 = release immediately).
void mut_move(uint32_t vid, float angle_deg, uint8_t func, float tx, float ty,
uint32_t dur_ms, uint8_t arg = 0, uint32_t chk_time = 0);
// 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);
// CLIENT-GAP §3.7 — knockback-motion latch (client-only, animation-bound).
// Set true when a knockdown .msa starts, false when it ends; never from a timer.
void mut_knockdown(uint32_t vid, bool on);
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<ShopTab> &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<NPCMark> &marks);
void mut_land_areas(const std::vector<LandArea> &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_lover_online(bool online);
void mut_lover_near(bool near);
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 <name>` 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);
// `sockets` and `attrs` mirror the 40250 GC_EXCHANGE item payload. They are
// optional for synthetic/server-command updates and default to zero.
void mut_exchange_item(bool self, uint8_t slot, uint32_t vnum, uint8_t count,
const int32_t *sockets = nullptr, const ItemAttr *attrs = nullptr);
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(uint8_t code); // EXCHANGE_GC_ALREADY / LESS_GOLD 子头值
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<uint32_t> 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<int> drain_cooldown_ends() {
auto v = std::move(m_cooldown_ends);
m_cooldown_ends.clear();
return v;
}
std::vector<FlyCue> drain_fly_cues() {
auto v = std::move(m_fly_cues);
m_fly_cues.clear();
return v;
}
std::vector<FlyTargetCue> drain_fly_target_cues() {
auto v = std::move(m_fly_target_cues);
m_fly_target_cues.clear();
return v;
}
std::vector<ScriptCue> drain_scripts() {
auto v = std::move(m_scripts);
m_scripts.clear();
return v;
}
std::vector<ConfirmCue> drain_confirms() {
auto v = std::move(m_confirms);
m_confirms.clear();
return v;
}
// vids of quest-log entries changed since last drain.
std::vector<uint16_t> 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<uint16_t> quest_indices() const {
std::vector<uint16_t> 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<uint32_t> party_pids() const {
std::vector<uint32_t> 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<uint32_t> drain_party_invites() {
auto v = std::move(m_party_invites);
m_party_invites.clear();
return v;
}
// --- P8 messenger ---
std::vector<Friend> friends() const {
std::vector<Friend> 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<std::string> 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<ShopEntry> &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<ShopTab> &shop_tabs() const { return m_shop_tabs; }
bool shop_dirty() { bool d = m_shop_dirty; m_shop_dirty = false; return d; }
std::vector<std::string> 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<std::vector<CubeMaterialSlot>> 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<CubeResultEntry> results; // r_list: what this NPC can make
std::vector<CubeRecipe> recipes; // m_info: materials per result
};
enum class CubeEvent { Opened, Closed, InfoChanged, Success, Fail };
const CubeState &cube() const { return m_cube; }
std::vector<CubeEvent> 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<GuildWarPair> &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<GuildComment> &comments);
void mut_guild_invite(uint32_t guild_id, const std::string &guild_name);
const GuildState &guild() const { return m_guild; }
std::vector<GuildMember> guild_members() const {
std::vector<GuildMember> 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<GuildComment> &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<GuildInvite> 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<GuildWarPair> &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<GuildWarStatus> drain_guild_war_events() {
auto v = std::move(m_guild_war_events);
m_guild_war_events.clear();
return v;
}
std::vector<GuildWarScore> drain_guild_war_scores() {
auto v = std::move(m_guild_war_scores);
m_guild_war_scores.clear();
return v;
}
// --- refine ---
std::vector<RefineCue> drain_refine_cues() {
auto v = std::move(m_refine_cues);
m_refine_cues.clear();
return v;
}
std::vector<DragonSoulCue> 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<WarpCue> 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<NPCMark> &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<WorldMarker> markers() const {
std::vector<WorldMarker> 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<uint32_t> 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<uint32_t> ground_vids() const;
std::vector<InvChange> drain_inv() {
auto v = std::move(m_inv_changes);
m_inv_changes.clear();
return v;
}
std::vector<GroundChange> drain_ground() {
auto v = std::move(m_ground_changes);
m_ground_changes.clear();
return v;
}
std::vector<uint32_t> drain_view_equipment_changes() {
auto v = std::move(m_view_equipment_changes);
m_view_equipment_changes.clear();
return v;
}
std::vector<ItemEvent> drain_item_events() {
auto v = std::move(m_item_events);
m_item_events.clear();
return v;
}
std::vector<PvpRelation> drain_pvp_changes() {
auto v = std::move(m_pvp_changes);
m_pvp_changes.clear();
return v;
}
std::vector<PvpRelation> pvp_relations() const;
const std::vector<uint32_t> &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<Change> 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<uint32_t> 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<ChatMsg> drain_chat() {
auto v = std::move(m_chat);
m_chat.clear();
return v;
}
std::vector<ServerCommandEvent> 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<uint32_t> drain_vitals() {
auto v = std::move(m_vitals);
m_vitals.clear();
return v;
}
std::vector<DamageEvent> drain_damage() {
auto v = std::move(m_damage);
m_damage.clear();
return v;
}
std::vector<MotionEvent> drain_motions() {
auto v = std::move(m_motions);
m_motions.clear();
return v;
}
std::vector<DigMotionEvent> drain_dig_motions() {
auto v = std::move(m_dig_motions);
m_dig_motions.clear();
return v;
}
std::vector<FishingEvent> drain_fishing_events() {
auto v = std::move(m_fishing_events);
m_fishing_events.clear();
return v;
}
std::vector<DungeonEvent> drain_dungeon_events() {
auto v = std::move(m_dungeon_events);
m_dungeon_events.clear();
return v;
}
const std::vector<LandArea> &land_areas() const { return m_land_areas; }
bool take_land_dirty() { bool d = m_land_dirty; m_land_dirty = false; return d; }
std::vector<Observer> observers() const {
std::vector<Observer> v;
v.reserve(m_observers.size());
for (auto &kv : m_observers) v.push_back(kv.second);
return v;
}
std::vector<ObserverEvent> drain_observer_events() {
auto v = std::move(m_observer_events);
m_observer_events.clear();
return v;
}
std::vector<AffectChange> drain_affects() {
auto v = std::move(m_affect_changes);
m_affect_changes.clear();
return v;
}
std::vector<EffectCue> drain_effect_cues() {
auto v = std::move(m_effect_cues);
m_effect_cues.clear();
return v;
}
std::vector<Affect> affects() const {
std::vector<Affect> 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 mark_vitals(uint32_t vid);
Item *mut_slot(uint8_t window, uint16_t cell);
// CLIENT-GAP §3.2 helpers for the TCP state queue.
void drain_state_queue(Entity &e); // release due entries for one entity
void apply_state_cmd(Entity &e, const StateCmd &c); // the FUNC_* switch (1.0 / 50.0 thresholds)
void begin_state_walk(Entity &e, const StateCmd &c, uint8_t after_func); // Src/Dst + skip-collision
static bool can_process_network_state(const Entity &e); // ~ __CanProcessNetworkStatePacket
static bool is_enable_tcp_process(const Entity &e, uint8_t func); // ~ __IsEnableTCPProcess
std::unordered_map<uint32_t, Entity> m_ents;
uint32_t m_main_vid = 0;
uint32_t m_now = 0;
uint32_t m_server_frame_ms = 0;
std::vector<Change> m_changes;
std::vector<uint32_t> m_dirty;
std::string m_bgm_name;
float m_bgm_volume = -1.0f;
bool m_bgm_dirty = false;
std::vector<ChatMsg> m_chat;
std::vector<uint32_t> m_vitals;
std::vector<DamageEvent> m_damage;
std::vector<MotionEvent> m_motions;
std::vector<DigMotionEvent> m_dig_motions;
std::vector<FishingEvent> m_fishing_events;
std::vector<DungeonEvent> m_dungeon_events;
std::vector<LandArea> m_land_areas;
bool m_land_dirty = false;
std::unordered_map<uint32_t, Observer> m_observers;
std::vector<ObserverEvent> m_observer_events;
std::unordered_map<uint32_t, Affect> m_affects;
std::vector<AffectChange> m_affect_changes;
std::vector<EffectCue> m_effect_cues;
std::vector<FlyCue> m_fly_cues;
std::vector<FlyTargetCue> 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<int> m_cooldown_ends;
std::vector<ScriptCue> m_scripts;
std::vector<ConfirmCue> m_confirms;
std::unordered_map<uint16_t, QuestInfo> m_quests;
std::vector<uint16_t> 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<uint32_t, ViewedEquipment> m_view_equipment;
std::unordered_map<uint32_t, GroundItem> m_ground;
std::vector<InvChange> m_inv_changes;
std::vector<GroundChange> m_ground_changes;
std::vector<uint32_t> m_view_equipment_changes;
std::vector<ItemEvent> m_item_events;
std::unordered_map<uint64_t, PvpRelation> m_pvp;
std::vector<PvpRelation> m_pvp_changes;
std::vector<uint32_t> 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<uint32_t, PartyMember> m_party;
uint8_t m_party_mode = 0;
bool m_party_dirty = false;
std::vector<uint32_t> m_party_invites;
std::unordered_map<std::string, Friend> m_friends;
bool m_friends_dirty = false;
std::vector<std::string> m_friend_invites;
bool m_shop_open = false;
uint32_t m_shop_vid = 0;
std::vector<ShopEntry> m_shop_items; // == m_shop_tabs[0].items when tabs present
std::vector<ShopTab> m_shop_tabs;
bool m_shop_dirty = false;
int32_t m_shop_selling_price = 0;
std::vector<std::string> 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<CubeEvent> m_cube_events;
CubeResultEntry m_cube_last_success;
std::vector<ServerCommandEvent> m_server_commands;
void apply_server_command(const std::string &line);
// P9 world systems
std::vector<WarpCue> m_warps;
int64_t m_server_time = 0;
bool m_time_dirty = false;
int m_channel = 0;
bool m_channel_dirty = false;
std::vector<NPCMark> m_npc_marks;
bool m_npc_marks_dirty = false;
std::unordered_map<int32_t, WorldMarker> m_markers;
bool m_markers_dirty = false;
std::vector<uint32_t> m_mount_changes;
// guild / refine
GuildState m_guild;
std::unordered_map<uint32_t, GuildMember> m_guild_members;
std::vector<GuildComment> m_guild_comments;
std::vector<GuildInvite> 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<GuildWarPair> m_guild_wars;
bool m_guild_war_dirty = false;
std::unordered_map<uint32_t, std::string> m_guild_names;
std::vector<GuildWarStatus> m_guild_war_events;
std::vector<GuildWarScore> m_guild_war_scores;
std::vector<RefineCue> m_refine_cues;
// dragon-soul refine
Item m_dragon_soul[DRAGON_SOUL_MAX_NUM];
std::vector<DragonSoulCue> m_ds_cues;
};
} // namespace mtnet