40250 classic W2: EntityStore data-layer only + net_world visibility (增量 50)
CLIENT-GAP-FIX §2.1/§2.3/§2.4/§2.5 (+ §2.2 steps 4-5): - §2.1 two-layer model: EntityStore is data-only. Entries are created only by mut_spawn / mut_spawn_full / mut_spawn_main and removed only by mut_despawn / main-vid table clear. Every other mut_* now does find-or-return with a MT_NET_TRACE-gated debug log; no pending / replay buffer. Visibility moves to net_world.gd::_update_visibility() — per frame it walks client.get_entities() and mirrors is_main / AFFECT_SHOW_ALWAYS(bit 7) / IsWall(14201-14204) / planar-distance < CHAR_STAGE_VIEW_BOUND (200*100 cm), building a node on entry and fading + freeing it past +10 while keeping the data row (_fading guard). Gate is fail-open when the data layer is not queryable (no get_entities / _main_vid == 0 / no pos_cm) so existing fake-client tests are unaffected. - §2.3 one overwrite rule: mut_spawn_full() does a clean whole-row value replace (m_ents[vid] = e), no preservation of prior move state on VID reuse; mut_spawn() now delegates to it. net_world._on_spawn destroys + recreates a node when the vid already has one; mount/dismount rebuild keeps the old global_position. - §2.4 points bounds: mut_set_points() clamps counts > POINT_MAX_NUM (255) and zeroes the tail of a short snapshot; mut_set_point() rejects type >= 255. - §2.5 field apply order + affect flags: new EntityStore::mut_affect_flags(vid, lo, hi) assembles v = lo | (hi << 32) and queues a dirty tick; mut_char_update guild/alignment/pk changes now also push Info. net_world._apply_field_updates() applies fields in the reference call order (NetworkActorManager.cpp:470). - §2.2 steps 4-5: mut_char_info() / mut_shop_sign() drop the touch()-creates- entity path. Steps 1-3 (m_pending_actor two-packet merge) remain W1-owned. Tests: ./build.sh Debug exit 0; ctest 16/16 (net.entity_store gains a §2.1 data-layer assertion block). New project/net_world_vis_test.gd for scene-node visibility. GDScript regressions green: netbridge_test, gamescene_test, netplay_test, p9_test, combat_fx_test, player_motion_test, net_world_vis_test. Follow-up for W1: call mut_affect_flags(vid, affect_flag[0], affect_flag[1]) from classic_parser.cpp after GC_CHARACTER_ADD[2] / GC_CHARACTER_UPDATE (mut_char_update has no affect param). wire_classic.h already has the wanted static_assert on GCPoints.points. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f32064c74a
commit
3487df2a16
@@ -1,6 +1,8 @@
|
||||
#include "entity_store.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
@@ -30,6 +32,17 @@ constexpr uint16_t MOTION_CHEERUP = 347;
|
||||
constexpr uint16_t MOTION_BANTER = 348;
|
||||
constexpr uint16_t MOTION_JOY = 349;
|
||||
|
||||
// CLIENT-GAP §2.1/§2.2: the data-layer mutators never create an entity except
|
||||
// through mut_spawn / mut_spawn_full / mut_spawn_main. Every other mut_* that is
|
||||
// handed an unknown VID logs (when MT_NET_TRACE is set) and returns instead of
|
||||
// silently spawning a half-populated row or buffering a replay.
|
||||
void dbg_ignored(const char *what, uint32_t id) {
|
||||
static const bool trace = std::getenv("MT_NET_TRACE") != nullptr;
|
||||
if (trace) {
|
||||
std::fprintf(stderr, "[entity_store] %s: unknown VID %u -> ignored\n", what, id);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Entity &EntityStore::touch(uint32_t vid, bool &created) {
|
||||
@@ -75,13 +88,15 @@ void EntityStore::start_move(Entity &e, float tx, float ty, uint32_t start_ms, u
|
||||
|
||||
void EntityStore::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, uint8_t attack_speed) {
|
||||
bool created;
|
||||
Entity &e = touch(vid, created);
|
||||
// CLIENT-GAP §2.3: a plain character-add is just a full spawn with the
|
||||
// remaining fields left at their defaults. Route it through mut_spawn_full so
|
||||
// there is exactly one "whole row replace" code path and VID reuse always
|
||||
// resets transient state (dead / stun / mount / guild / affects / move).
|
||||
Entity e{};
|
||||
e.vid = vid;
|
||||
e.race = race;
|
||||
e.ch_type = ch_type;
|
||||
if (!name.empty()) {
|
||||
e.name = name;
|
||||
}
|
||||
e.name = name;
|
||||
e.x = x;
|
||||
e.y = y;
|
||||
e.z = z;
|
||||
@@ -89,7 +104,7 @@ void EntityStore::mut_spawn(uint32_t vid, uint16_t race, uint8_t ch_type, const
|
||||
e.moving_speed = moving_speed;
|
||||
e.attack_speed = attack_speed;
|
||||
e.moving = false;
|
||||
m_changes.push_back({ChangeKind::Spawn, vid});
|
||||
mut_spawn_full(e);
|
||||
}
|
||||
|
||||
void EntityStore::mut_spawn_main(uint32_t vid, uint16_t race, const std::string &name, float x,
|
||||
@@ -113,22 +128,18 @@ void EntityStore::mut_spawn_main(uint32_t vid, uint16_t race, const std::string
|
||||
}
|
||||
|
||||
void EntityStore::mut_spawn_full(const Entity &src) {
|
||||
bool created;
|
||||
Entity &e = touch(src.vid, created);
|
||||
// Preserve the live interpolation state if the entity is already moving and
|
||||
// the caller did not carry an explicit move (spawn packets never do).
|
||||
const bool was_moving = e.moving;
|
||||
const float keep_x = e.x, keep_y = e.y, keep_z = e.z;
|
||||
e = src;
|
||||
// CLIENT-GAP §2.3 "one overwrite rule": a spawn replaces the whole row keyed
|
||||
// by vid. The reference (SNetworkActorData::__copy__) copies every field from
|
||||
// the incoming data on VID reuse and does NOT preserve a prior in-progress
|
||||
// move — so we do a clean value replace here. Transient state (dead, stun,
|
||||
// mount, guild, affect_flags, func/move) is whatever `src` carries, which for
|
||||
// a fresh Entity{} is the default. Copy into a local first to stay safe if a
|
||||
// caller ever passes `store.get(vid)` back in.
|
||||
Entity e = src;
|
||||
e.vid = src.vid;
|
||||
if (was_moving && !src.moving) {
|
||||
e.moving = true;
|
||||
e.x = keep_x;
|
||||
e.y = keep_y;
|
||||
e.z = keep_z;
|
||||
}
|
||||
m_ents[src.vid] = e;
|
||||
if (e.is_main) {
|
||||
m_main_vid = e.vid;
|
||||
m_main_vid = src.vid;
|
||||
}
|
||||
m_changes.push_back({ChangeKind::Spawn, src.vid});
|
||||
}
|
||||
@@ -144,6 +155,23 @@ void EntityStore::mut_ownership(uint32_t vid, uint32_t owner_vid) {
|
||||
}
|
||||
}
|
||||
|
||||
void EntityStore::mut_affect_flags(uint32_t vid, uint32_t lo, uint32_t hi) {
|
||||
// CLIENT-GAP §2.5: GC_CHARACTER_ADD[2] / GC_CHARACTER_UPDATE carry the affect
|
||||
// bitset as two 32-bit words (client CopyData(0,4,&flag[0]) +
|
||||
// CopyData(32,4,&flag[1])); re-assemble as lo | (hi << 32). No-op for an
|
||||
// unknown VID — the bitset lives on the entity row only.
|
||||
auto it = m_ents.find(vid);
|
||||
if (it == m_ents.end()) {
|
||||
dbg_ignored("mut_affect_flags", vid);
|
||||
return;
|
||||
}
|
||||
const uint64_t v = (uint64_t)lo | ((uint64_t)hi << 32);
|
||||
if (it->second.affect_flags != v) {
|
||||
it->second.affect_flags = v;
|
||||
m_dirty.push_back(vid);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityStore::mut_map_bgm(const std::string &name, float volume) {
|
||||
bool changed = false;
|
||||
if (name != m_bgm_name) {
|
||||
@@ -181,10 +209,21 @@ void EntityStore::mut_move(uint32_t vid, float angle_deg, uint8_t func, float tx
|
||||
}
|
||||
|
||||
void EntityStore::mut_set_points(const int32_t *pts, int n) {
|
||||
if (n > 256) {
|
||||
n = 256;
|
||||
// CLIENT-GAP §2.4: GC_CHARACTER_POINTS is a full snapshot. The wire array is
|
||||
// exactly POINT_MAX_NUM (255) ints; clamp a longer count and zero the tail of
|
||||
// a shorter one so a stale value can never survive under a fresh snapshot.
|
||||
// (W1 owns wire_classic.h; GCPoints there already carries
|
||||
// static_assert(sizeof(GCPoints) == 1 + 255 * 4), i.e. points[POINT_MAX_NUM].)
|
||||
if (!pts || n <= 0) {
|
||||
return;
|
||||
}
|
||||
if (n > POINT_MAX_NUM) {
|
||||
n = POINT_MAX_NUM;
|
||||
}
|
||||
std::memcpy(m_points.v, pts, sizeof(int32_t) * (size_t)n);
|
||||
for (int i = n; i < POINT_MAX_NUM; ++i) {
|
||||
m_points.v[i] = 0;
|
||||
}
|
||||
m_current_stamina = (uint32_t)std::max(0, m_points.stamina());
|
||||
m_stamina_current_f = (float)m_current_stamina;
|
||||
m_points_dirty = true;
|
||||
@@ -206,6 +245,12 @@ void EntityStore::mut_set_points(const int32_t *pts, int n) {
|
||||
}
|
||||
|
||||
void EntityStore::mut_set_point(uint8_t type, int32_t value, uint32_t vid) {
|
||||
// CLIENT-GAP §2.4: a single-index point change must stay inside the array.
|
||||
// Valid indices are 0..POINT_MAX_NUM-1 (0..254).
|
||||
if (type >= POINT_MAX_NUM) {
|
||||
dbg_ignored("mut_set_point index out of range", type);
|
||||
return;
|
||||
}
|
||||
if (vid == m_main_vid) {
|
||||
m_points.v[type] = value;
|
||||
if (type == POINT_STAMINA) {
|
||||
@@ -334,9 +379,14 @@ void EntityStore::mut_char_update(uint32_t vid, const uint16_t *parts, uint8_t m
|
||||
e.attack_speed = attack_speed;
|
||||
changed = true;
|
||||
}
|
||||
e.guild = guild_id;
|
||||
e.alignment = alignment;
|
||||
e.pk_mode = pk_mode;
|
||||
// CLIENT-GAP §2.5: guild / alignment / pk-mode are part of the node refresh
|
||||
// apply order, so a change to any of them must re-mirror the entity too.
|
||||
if (e.guild != guild_id || e.alignment != alignment || e.pk_mode != pk_mode) {
|
||||
e.guild = guild_id;
|
||||
e.alignment = alignment;
|
||||
e.pk_mode = pk_mode;
|
||||
changed = true;
|
||||
}
|
||||
if (e.mount_vnum != mount_vnum) {
|
||||
e.mount_vnum = mount_vnum;
|
||||
m_mount_changes.push_back(vid);
|
||||
@@ -424,13 +474,17 @@ void EntityStore::mut_damage(uint32_t vid, uint8_t flag, int32_t amount) {
|
||||
}
|
||||
|
||||
void EntityStore::mut_mount(uint32_t vid, uint32_t mount_vnum) {
|
||||
bool created = false;
|
||||
Entity &e = touch(vid, created);
|
||||
e.mount_vnum = mount_vnum;
|
||||
m_mount_changes.push_back(vid);
|
||||
if (!created) {
|
||||
m_changes.push_back({ChangeKind::Info, vid});
|
||||
// CLIENT-GAP §2.1: mount toggles never create an entity. A GC_MOUNT for a VID
|
||||
// we have not spawned is dropped (the server always sends the character-add
|
||||
// first).
|
||||
auto it = m_ents.find(vid);
|
||||
if (it == m_ents.end()) {
|
||||
dbg_ignored("mut_mount", vid);
|
||||
return;
|
||||
}
|
||||
it->second.mount_vnum = mount_vnum;
|
||||
m_mount_changes.push_back(vid);
|
||||
m_changes.push_back({ChangeKind::Info, vid});
|
||||
}
|
||||
|
||||
void EntityStore::mut_fly(uint8_t type, uint32_t start_vid, uint32_t end_vid) {
|
||||
@@ -506,8 +560,15 @@ void EntityStore::mut_view_equipment(uint32_t vid, const Item *items, int n) {
|
||||
void EntityStore::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) {
|
||||
bool created;
|
||||
Entity &e = touch(vid, created);
|
||||
// CLIENT-GAP §2.2: GC_CHAR_ADDITIONAL_INFO decorates an entity the spawn
|
||||
// packet already created. An unknown VID means we missed (or already culled)
|
||||
// the add — drop it instead of touch()-ing a nameless placeholder into being.
|
||||
auto it = m_ents.find(vid);
|
||||
if (it == m_ents.end()) {
|
||||
dbg_ignored("mut_char_info", vid);
|
||||
return;
|
||||
}
|
||||
Entity &e = it->second;
|
||||
if (!name.empty()) {
|
||||
e.name = name;
|
||||
}
|
||||
@@ -525,24 +586,16 @@ void EntityStore::mut_char_info(uint32_t vid, const std::string &name, const uin
|
||||
if (empire) {
|
||||
e.empire = empire;
|
||||
}
|
||||
if (!created) {
|
||||
m_changes.push_back({ChangeKind::Info, vid});
|
||||
}
|
||||
m_changes.push_back({ChangeKind::Info, vid});
|
||||
}
|
||||
|
||||
void EntityStore::mut_shop_sign(uint32_t vid, const std::string &sign) {
|
||||
// CLIENT-GAP §2.2: the sign is a field on the entity that owns the private
|
||||
// shop. An unknown VID never spawns a bare row here — the character-add
|
||||
// packet is the only thing that creates entities. Drop it and log.
|
||||
auto it = m_ents.find(vid);
|
||||
if (it == m_ents.end()) {
|
||||
// A non-empty sign can legally race the character-add packet during a
|
||||
// shop broadcast. Keep it instead of silently dropping the server state;
|
||||
// the later character-add packet fills in the rest of this entity.
|
||||
if (sign.empty()) {
|
||||
return;
|
||||
}
|
||||
bool created = false;
|
||||
Entity &e = touch(vid, created);
|
||||
e.shop_sign = sign;
|
||||
m_changes.push_back({ChangeKind::Spawn, vid});
|
||||
dbg_ignored("mut_shop_sign", vid);
|
||||
return;
|
||||
}
|
||||
if (it->second.shop_sign == sign) {
|
||||
|
||||
@@ -481,6 +481,10 @@ public:
|
||||
void mut_spawn_full(const Entity &e);
|
||||
// summon / horse ownership (CLIENT-GAP §2.5); no-op if vid unknown.
|
||||
void mut_ownership(uint32_t vid, uint32_t owner_vid);
|
||||
// Affect bitset from GC_CHARACTER_ADD[2] / GC_CHARACTER_UPDATE, delivered as
|
||||
// the two 32-bit wire words (CLIENT-GAP §2.5: v = lo | (hi << 32)). Queues a
|
||||
// dirty tick so net_world re-mirrors the node; no-op if vid unknown.
|
||||
void mut_affect_flags(uint32_t vid, uint32_t lo, uint32_t hi);
|
||||
// Map background music (CLIENT-GAP §9.1). name is the resolved track id or
|
||||
// path; volume < 0 means "server did not specify, keep current".
|
||||
void mut_map_bgm(const std::string &name, float volume);
|
||||
|
||||
Reference in New Issue
Block a user