Captures the uncommitted parallel-development work (increments 46-49) on the
40250 classic client port, folded into docs/CLIENT-GAP.md + CLIENT-GAP-FIX.md.
Increment 49 (W0 interface-freeze batch, Phase 1 prerequisite) lands the
cross-workflow shared-file scaffolding so the 4 Phase 1 workflows can run in
isolated worktrees without contention:
- entity_store.{h,cpp}: Entity gains empire/affect_flags/owner_vid/state_flags;
new mut_spawn_full() (single-shot two-packet merge, §2.3), mut_ownership()
(§2.5), mut_map_bgm() + take_bgm_dirty()/bgm_name()/bgm_volume() (§9.1),
drain_dirty() (§2.1/§2.5 bare-field-update queue); mut_char_info() now stores
empire; reset_for_map_change() clears m_dirty.
- m2_client.cpp: new bgm_changed(name, volume) signal; classic + m2dev pump
loops drain drain_dirty() -> entity_info and take_bgm_dirty() -> bgm_changed;
entity_dict() exposes the 4 new keys.
- classic/classic_parser.{h,cpp}: GC_MAIN_CHARACTER3_BGM /
GC_MAIN_CHARACTER4_BGM_VOL route bgm_name/bgm_volume to mut_map_bgm() (was
discarded); new m_pending_actor staging map (W1 fills §2.2 merge logic).
- project/bgm_director.gd (new) + game_scene.gd: BGM consumption split out of
net_world.gd so W2/W4 don't collide; §8.6/§4.8 keybind convergence (digits
1-4 -> quickslots 0-3, F1-F4 -> quickslots 4-7, Ctrl+1..9 -> _emote()).
Tests: cmake --build build clean; ctest 16/16; 15 GDScript regressions green
(netbridge, gamescene, netplay, p9, p10, p2b, p8, system_menu_ui, skill,
combat_fx, skill_fx, player_motion, char_status_ui, chat, inventory), no skips.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
1239 lines
42 KiB
C++
1239 lines
42 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;
|
|
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<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;
|
|
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 <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 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<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_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);
|
|
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<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 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<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
|