Implement 40250 classic client port

This commit is contained in:
shenlei
2026-09-01 18:49:28 +09:00
parent 2277b1dd60
commit 8f61990a83
481 changed files with 169826 additions and 293 deletions
+270
View File
@@ -32,6 +32,7 @@ struct Entity {
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;
@@ -230,6 +231,7 @@ struct PartyMember {
struct Friend {
std::string name;
bool online = false;
bool mobile = false; // 40250 GC_MESSENGER_MOBILE / connected-state bit
};
// --- P8 NPC shop (GC_SHOP) ----------------------------------------------------
@@ -269,11 +271,21 @@ 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;
@@ -333,6 +345,37 @@ struct RefineCue {
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;
@@ -381,6 +424,8 @@ struct PlayerPoints {
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]; }
@@ -408,8 +453,158 @@ public:
// 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);
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);
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();
@@ -522,10 +717,16 @@ public:
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; }
@@ -588,6 +789,29 @@ public:
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;
@@ -602,6 +826,17 @@ public:
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; }
@@ -637,6 +872,10 @@ public:
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 ---
@@ -703,12 +942,21 @@ public:
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;
@@ -725,6 +973,11 @@ public:
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);
@@ -855,9 +1108,20 @@ private:
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;
@@ -866,11 +1130,13 @@ private:
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;
@@ -888,6 +1154,7 @@ private:
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
@@ -905,8 +1172,11 @@ private:
// 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;