Files
mtgodot-poc/extension/src/net/entity_store.h
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

926 lines
27 KiB
C++

#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 <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,
};
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;
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;
};
// --- P9 world systems ----------------------------------------------------
// GC_WARP — teleport target. same_server() -> just move the player; otherwise
// the caller must reconnect to addr:port (P10).
struct WarpCue {
int32_t x = 0, y = 0;
int32_t addr = 0;
uint16_t port = 0;
bool same_server() const { return 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;
};
// --- 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;
};
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;
uint8_t job = 0;
uint8_t level = 0;
uint32_t offer = 0;
std::string 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;
};
// 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;
};
// 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 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.
void apply(uint16_t header, const void *body, uint16_t len);
// 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; }
// --- P8 NPC shop ---
bool shop_open() const { return m_shop_open; }
uint32_t shop_vid() const { return m_shop_vid; }
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 ---
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; }
// --- 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;
}
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;
bool take_duel_started() {
bool started = m_duel_started;
m_duel_started = false;
return started;
}
const LoverInfo &lover() const { return m_lover; }
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;
}
std::vector<ChatMsg> drain_chat() {
auto v = std::move(m_chat);
m_chat.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 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<uint32_t, Entity> m_ents;
uint32_t m_main_vid = 0;
uint32_t m_now = 0;
std::vector<Change> m_changes;
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;
bool m_duel_started = false;
LoverInfo m_lover;
bool m_lover_dirty = false;
// 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;
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;
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;
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;
GuildGrade m_guild_grades[16];
bool m_guild_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