From 3675f1d8084486164b118fd5131b5f66bcd142c5 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:20:54 +0100 Subject: [PATCH 1/8] Extended usage of Flecs --- .../server/scripting/builtins/entity.cpp | 20 +++-- code/framework/src/world/client.cpp | 31 ++++--- code/framework/src/world/client.h | 3 + code/framework/src/world/engine.cpp | 36 +++++--- code/framework/src/world/engine.h | 4 + code/framework/src/world/modules/base.hpp | 65 ++++++++------ code/framework/src/world/server.cpp | 85 ++++++++++++++----- code/framework/src/world/server.h | 6 ++ 8 files changed, 179 insertions(+), 71 deletions(-) diff --git a/code/framework/src/integrations/server/scripting/builtins/entity.cpp b/code/framework/src/integrations/server/scripting/builtins/entity.cpp index 59de2c78e..72ac0a169 100644 --- a/code/framework/src/integrations/server/scripting/builtins/entity.cpp +++ b/code/framework/src/integrations/server/scripting/builtins/entity.cpp @@ -158,23 +158,27 @@ namespace Framework::Integrations::Scripting { } void Entity::SetVisible(bool visible) const { - const auto st = _ent.get_mut(); - st->isVisible = visible; + if (visible) { + _ent.remove(); + } else { + _ent.add(); + } } void Entity::SetAlwaysVisible(bool visible) const { - const auto st = _ent.get_mut(); - st->alwaysVisible = visible; + if (visible) { + _ent.add(); + } else { + _ent.remove(); + } } bool Entity::IsVisible() const { - const auto st = _ent.get(); - return st->isVisible; + return !_ent.has(); } bool Entity::IsAlwaysVisible() const { - const auto st = _ent.get(); - return st->alwaysVisible; + return _ent.has(); } void Entity::SetVirtualWorld(int virtualWorld) const { diff --git a/code/framework/src/world/client.cpp b/code/framework/src/world/client.cpp index 9a4ba5e44..b68382c6f 100644 --- a/code/framework/src/world/client.cpp +++ b/code/framework/src/world/client.cpp @@ -21,6 +21,19 @@ namespace Framework::World { _queryGetEntityByServerID = _world->query_builder().build(); + // Observer to maintain ServerID cache + _world->observer("ServerIDCacheUpdate") + .event(flecs::OnSet) + .each([this](flecs::entity e, Modules::Base::ServerID& sid) { + _serverIdCache[sid.id] = e; + }); + + _world->observer("ServerIDCacheRemove") + .event(flecs::OnRemove) + .each([this](flecs::entity e, Modules::Base::ServerID& sid) { + _serverIdCache.erase(sid.id); + }); + return EngineError::ENGINE_NONE; } @@ -33,14 +46,11 @@ namespace Framework::World { } flecs::entity ClientEngine::GetEntityByServerID(flecs::entity_t id) const { - flecs::entity ent = {}; - _queryGetEntityByServerID.each([&ent, id](flecs::entity e, Modules::Base::ServerID& rhs) { - if (id == rhs.id) { - ent = e; - return; - } - }); - return ent; + auto it = _serverIdCache.find(id); + if (it != _serverIdCache.end() && it->second.is_alive()) { + return it->second; + } + return flecs::entity::null(); } flecs::entity_t ClientEngine::GetServerID(flecs::entity entity) { @@ -73,9 +83,10 @@ namespace Framework::World { for (auto i : it) { const auto &es = &rs[i]; + const auto e = it.entity(i); - if (es->GetBaseEvents().updateProc && es->performTickUpdates && Framework::World::Engine::IsEntityOwner(it.entity(i), myGUID.g)) { - es->GetBaseEvents().updateProc(_networkPeer, (SLNet::UNASSIGNED_RAKNET_GUID).g, it.entity(i)); + if (es->GetBaseEvents().updateProc && !e.has() && Framework::World::Engine::IsEntityOwner(e, myGUID.g)) { + es->GetBaseEvents().updateProc(_networkPeer, (SLNet::UNASSIGNED_RAKNET_GUID).g, e); } } } diff --git a/code/framework/src/world/client.h b/code/framework/src/world/client.h index 0f3630079..3f531af71 100644 --- a/code/framework/src/world/client.h +++ b/code/framework/src/world/client.h @@ -47,6 +47,9 @@ namespace Framework::World { flecs::query _queryGetEntityByServerID; OnEntityDestroyCallback _onEntityDestroyCallback; + // Cache for O(1) ServerID lookups + std::unordered_map _serverIdCache; + private: void InitRPCs(Networking::NetworkPeer *peer) const; diff --git a/code/framework/src/world/engine.cpp b/code/framework/src/world/engine.cpp index c402d2cd2..f91fdce42 100644 --- a/code/framework/src/world/engine.cpp +++ b/code/framework/src/world/engine.cpp @@ -22,6 +22,19 @@ namespace Framework::World { _allStreamableEntities = _world->query_builder().build(); _findAllStreamerEntities = _world->query_builder().build(); + // Observer to maintain GUID cache + _world->observer("GUIDCacheUpdate") + .event(flecs::OnSet) + .each([this](flecs::entity e, Modules::Base::Streamer& s) { + _guidCache[s.guid] = e; + }); + + _world->observer("GUIDCacheRemove") + .event(flecs::OnRemove) + .each([this](flecs::entity e, Modules::Base::Streamer& s) { + _guidCache.erase(s.guid); + }); + return EngineError::ENGINE_NONE; } @@ -53,13 +66,11 @@ namespace Framework::World { } flecs::entity Engine::GetEntityByGUID(uint64_t guid) const { - flecs::entity ourEntity = {}; - _findAllStreamerEntities.each([&ourEntity, guid](flecs::entity e, Modules::Base::Streamer &s) { - if (ourEntity == flecs::entity::null() && s.guid == guid) { - ourEntity = e; - } - }); - return ourEntity; + auto it = _guidCache.find(guid); + if (it != _guidCache.end() && it->second.is_alive()) { + return it->second; + } + return flecs::entity::null(); } flecs::entity Engine::WrapEntity(flecs::entity_t serverID) const { @@ -68,9 +79,14 @@ namespace Framework::World { void Engine::PurgeAllResourceEntities() const { _world->defer_begin(); - _findAllResourceEntities.each([this](flecs::entity e, Modules::Base::RemovedOnResourceReload &rhs) { - if (e.is_alive()) - e.add(); + _findAllResourceEntities.run([this](flecs::iter& it) { + while (it.next()) { + for (auto i : it) { + auto e = it.entity(i); + if (e.is_alive()) + e.add(); + } + } }); _world->defer_end(); } diff --git a/code/framework/src/world/engine.h b/code/framework/src/world/engine.h index 3a585ee2a..f00a83b92 100644 --- a/code/framework/src/world/engine.h +++ b/code/framework/src/world/engine.h @@ -15,6 +15,7 @@ #include #include +#include #include "core_modules.h" @@ -57,6 +58,9 @@ namespace Framework::World { flecs::query _findAllResourceEntities; Networking::NetworkPeer *_networkPeer = nullptr; + // Cache for O(1) GUID lookups + std::unordered_map _guidCache; + public: EngineError Init(Networking::NetworkPeer *networkPeer); diff --git a/code/framework/src/world/modules/base.hpp b/code/framework/src/world/modules/base.hpp index 71aaeb0e2..ae221e15f 100644 --- a/code/framework/src/world/modules/base.hpp +++ b/code/framework/src/world/modules/base.hpp @@ -62,13 +62,31 @@ namespace Framework::World::Modules { std::string modelName; }; - struct PendingRemoval { - [[maybe_unused]] uint8_t _unused; - }; + struct PendingRemoval {}; - struct RemovedOnResourceReload { - [[maybe_unused]] uint8_t _unused; - }; + struct RemovedOnResourceReload {}; + + // Tag: Entity is hidden from streaming (inverse of isVisible) + struct Hidden {}; + + // Tag: Entity is always visible regardless of distance + struct AlwaysVisible {}; + + // Tag: Entity does not receive tick updates (inverse of performTickUpdates) + struct NoTickUpdates {}; + + // Tag: Entity ownership is assigned manually, not by proximity + struct ManualOwnership {}; + + // Tag: Custom visibility proc completely replaces framework heuristics + struct VisibilityReplace {}; + + // Tag: Custom visibility proc replaces only position/distance check + struct VisibilityReplacePosition {}; + + // Relation: Entity is owned by a streamer entity + // Usage: entity.add(streamerEntity) + struct OwnedBy {}; struct ServerID { flecs::entity_t id; @@ -80,21 +98,13 @@ namespace Framework::World::Modules { using OnDisconnectProc = fu2::function; using OnUpdateTransformProc = fu2::function; - enum class HeuristicMode { - ADD, - REPLACE, - REPLACE_POSITION - }; - int virtualWorld = 0; - bool isVisible = true; - bool alwaysVisible = false; + double defaultUpdateInterval = (1000.0 / 60.0); // 16.1667~ ms interval double updateInterval = defaultUpdateInterval; - uint64_t owner = 0; - // If set to true, the owner will not be assigned automatically by the framework - bool assignOwnerManually = false; + // Owner GUID for network synchronization (derived from OwnedBy relation on server) + uint64_t owner = 0; // Allows custom owner assignment logic, if method returns true we bypass framework's proximity based owner assignment AssignOwnerProc assignOwnerProc; @@ -116,18 +126,16 @@ namespace Framework::World::Modules { // Extra set of events so mod can supply custom data. Events modEvents; - // Custom visibility proc that either complements the existing heuristic or replaces it - HeuristicMode isVisibleHeuristic = HeuristicMode::ADD; + // Custom visibility proc for additional visibility checks + // Use VisibilityReplace tag to completely replace framework heuristics + // Use VisibilityReplacePosition tag to replace only distance check + // Without either tag, proc is combined with framework checks (AND) IsVisibleProc isVisibleProc; // Used to specify list of entities this streamable entity relies on. // If any of these entities are visible and ours is not, we force ours to be visible too. std::vector dependentEntities; - // Controls whether this entity gets to be updated continuously or not - // When set to false, we only stream spawn and despawn events, useful for immovable objects - bool performTickUpdates = true; - // Framework-level events. friend Base; @@ -174,6 +182,15 @@ namespace Framework::World::Modules { world.component(); world.component(); + // Visibility and behavior tags + world.component(); + world.component(); + world.component(); + world.component(); + world.component(); + world.component(); + world.component(); + // Windows bind metadata #ifdef _WIN32 { @@ -183,7 +200,7 @@ namespace Framework::World::Modules { _quat.member("w").member("x").member("y").member("z"); _transform.member("pos").member("rot").member("vel"); _frame.member("modelHash").member("scale"); - _streamable.member("virtualWorld").member("isVisible").member("alwaysVisible").member("updateInterval").member("owner"); + _streamable.member("virtualWorld").member("updateInterval").member("owner"); _streamer.member("range").member("guid"); } #endif diff --git a/code/framework/src/world/server.cpp b/code/framework/src/world/server.cpp index 411c12aa3..6c4a4746b 100644 --- a/code/framework/src/world/server.cpp +++ b/code/framework/src/world/server.cpp @@ -20,26 +20,51 @@ namespace Framework::World { _findAllResourceEntities = _world->query_builder().build(); - // Set up a system to remove entities we no longer need. - _world->system("RemoveEntities").kind(flecs::PostUpdate).interval(cfg.removeEntitiesTickInterval).each([this](flecs::entity e, Modules::Base::PendingRemoval &pd, Modules::Base::Streamable &streamable) { - // Remove the entity from all streamers. - _findAllStreamerEntities.each([this, &e, &streamable](flecs::entity rhsE, Modules::Base::Streamer &rhsS) { - if (rhsS.entities.find(e) != rhsS.entities.end()) { - rhsS.entities.erase(e); - - // Ensure we despawn the entity from the client. - if (streamable.GetBaseEvents().despawnProc) - streamable.GetBaseEvents().despawnProc(_networkPeer, rhsS.guid, e); + // Observer to sync OwnedBy relation to owner GUID for network messages + _world->observer() + .with(flecs::Wildcard) + .event(flecs::OnAdd) + .each([](flecs::iter& it, size_t row) { + auto e = it.entity(row); + auto owner = it.pair(0).second(); + auto streamable = e.get_mut(); + if (streamable && owner.is_valid()) { + auto streamer = owner.get(); + if (streamer) { + streamable->owner = streamer->guid; + } } }); - e.destruct(); + // Set up a system to remove entities we no longer need. + _world->system("RemoveEntities").kind(flecs::PostUpdate).interval(cfg.removeEntitiesTickInterval).run([this](flecs::iter &it) { + while (it.next()) { + auto streamables = it.field(1); + + for (auto i : it) { + auto e = it.entity(i); + auto &streamable = streamables[i]; + + // Remove the entity from all streamers. + _findAllStreamerEntities.each([this, &e, &streamable](flecs::entity rhsE, Modules::Base::Streamer &rhsS) { + if (rhsS.entities.find(e) != rhsS.entities.end()) { + rhsS.entities.erase(e); + + // Ensure we despawn the entity from the client. + if (streamable.GetBaseEvents().despawnProc) + streamable.GetBaseEvents().despawnProc(_networkPeer, rhsS.guid, e); + } + }); + + e.destruct(); + } + } }); // Set up a system to assign entity owners. _world->system("AssignEntityOwnership").kind(flecs::PostUpdate).interval(cfg.assignOwnershipTickInterval).each([this](flecs::entity e, Modules::Base::Transform &tr, Modules::Base::Streamable &streamable) { // Let user provide custom ownership assignment. - if (streamable.assignOwnerManually || (streamable.assignOwnerProc && streamable.assignOwnerProc(e, streamable))) { + if (e.has() || (streamable.assignOwnerProc && streamable.assignOwnerProc(e, streamable))) { /* no op */ } else { @@ -139,7 +164,7 @@ namespace Framework::World { return; // Let streamer send an update to self if an event is assigned. - if (e == it.entity(i) && rs[i].GetBaseEvents().selfUpdateProc && rs[i].performTickUpdates) { + if (e == it.entity(i) && rs[i].GetBaseEvents().selfUpdateProc && !e.has()) { rs[i].GetBaseEvents().selfUpdateProc(_networkPeer, s[i].guid, e); return; } @@ -162,7 +187,7 @@ namespace Framework::World { else if (rs[i].owner != otherS.owner) { auto &data = map_it->second; if (static_cast(Utils::Time::GetTime()) - data.lastUpdate > otherS.updateInterval) { - if (otherS.GetBaseEvents().updateProc && rs[i].performTickUpdates) + if (otherS.GetBaseEvents().updateProc && !e.has()) otherS.GetBaseEvents().updateProc(_networkPeer, s[i].guid, e); data.lastUpdate = static_cast(Utils::Time::GetTime()); } @@ -228,6 +253,27 @@ namespace Framework::World { return GetEntityByGUID(es->owner); } + void ServerEngine::SetOwnerRelation(flecs::entity e, flecs::entity owner) { + // Remove any existing ownership + e.remove(flecs::Wildcard); + // Add new ownership + if (owner.is_valid() && owner.is_alive()) { + e.add(owner); + } + } + + flecs::entity ServerEngine::GetOwnerRelation(flecs::entity e) const { + flecs::entity owner = flecs::entity::null(); + e.each([&owner](flecs::entity target) { + owner = target; + }); + return owner; + } + + bool ServerEngine::IsOwnedBy(flecs::entity e, flecs::entity owner) { + return e.has(owner); + } + std::vector ServerEngine::FindVisibleStreamers(flecs::entity e) const { std::vector streamers; const auto es = e.get(); @@ -277,7 +323,7 @@ namespace Framework::World { return false; // Allow user to override visibility rules completely. - if (rhsS.isVisibleProc && rhsS.isVisibleHeuristic == Modules::Base::Streamable::HeuristicMode::REPLACE) { + if (rhsS.isVisibleProc && e.has()) { return rhsS.isVisibleProc(streamerEntity, e); } @@ -307,11 +353,11 @@ namespace Framework::World { } // Entity is always visible to clients. - if (rhsS.alwaysVisible) + if (e.has()) return true; // Entity can be hidden from clients. - if (!rhsS.isVisible) + if (e.has()) return false; // Validate if the entity resides in the same virtual world client does. @@ -319,7 +365,7 @@ namespace Framework::World { return false; // Let user replace the distance check. - if (rhsS.isVisibleProc && rhsS.isVisibleHeuristic == Modules::Base::Streamable::HeuristicMode::REPLACE_POSITION) { + if (rhsS.isVisibleProc && e.has()) { return rhsS.isVisibleProc(streamerEntity, e); } @@ -334,7 +380,8 @@ namespace Framework::World { } // Allow user to provide additional rules for visibility. - if (rhsS.isVisibleProc && rhsS.isVisibleHeuristic == Modules::Base::Streamable::HeuristicMode::ADD) { + // ADD mode is default when neither Replace tag is present + if (rhsS.isVisibleProc && !e.has() && !e.has()) { isVisible = isVisible && rhsS.isVisibleProc(streamerEntity, e); } diff --git a/code/framework/src/world/server.h b/code/framework/src/world/server.h index 95dc10498..0c038fb7f 100644 --- a/code/framework/src/world/server.h +++ b/code/framework/src/world/server.h @@ -83,6 +83,12 @@ namespace Framework::World { static void SetOwner(flecs::entity e, uint64_t guid); flecs::entity GetOwner(flecs::entity e) const; + + // Ownership using Flecs relations + void SetOwnerRelation(flecs::entity e, flecs::entity owner); + flecs::entity GetOwnerRelation(flecs::entity e) const; + static bool IsOwnedBy(flecs::entity e, flecs::entity owner); + [[maybe_unused]] std::vector FindVisibleStreamers(flecs::entity e) const; bool IsEntityVisibleToStreamer(const flecs::entity streamerEntity, const flecs::entity e, const Modules::Base::Transform &lhsTr, const Modules::Base::Streamer &streamer, const Modules::Base::Streamable &lhsS, const Modules::Base::Transform &rhsTr, const Modules::Base::Streamable &rhsS) const; From eb6529f32f5ba95edaf795354f4c594489efe659 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:17:04 +0100 Subject: [PATCH 2/8] More Migration --- .../server/scripting/builtins/entity.cpp | 13 +- code/framework/src/world/client.cpp | 21 ++- code/framework/src/world/engine.cpp | 62 +++++++++ code/framework/src/world/engine.h | 9 ++ code/framework/src/world/modules/base.hpp | 76 +++++------ .../src/world/modules/modules_impl.cpp | 75 ----------- code/framework/src/world/server.cpp | 121 +++++++++--------- code/framework/src/world/server.h | 5 + code/framework/src/world/types/player.hpp | 6 +- code/framework/src/world/types/streaming.hpp | 15 +-- 10 files changed, 206 insertions(+), 197 deletions(-) diff --git a/code/framework/src/integrations/server/scripting/builtins/entity.cpp b/code/framework/src/integrations/server/scripting/builtins/entity.cpp index 72ac0a169..5f6cd7db3 100644 --- a/code/framework/src/integrations/server/scripting/builtins/entity.cpp +++ b/code/framework/src/integrations/server/scripting/builtins/entity.cpp @@ -182,13 +182,18 @@ namespace Framework::Integrations::Scripting { } void Entity::SetVirtualWorld(int virtualWorld) const { - const auto st = _ent.get_mut(); - st->virtualWorld = virtualWorld; + auto engine = Framework::CoreModules::GetWorldEngine(); + if (engine) { + engine->SetEntityVirtualWorld(_ent, virtualWorld); + } } int Entity::GetVirtualWorld() const { - const auto st = _ent.get(); - return st->virtualWorld; + auto engine = Framework::CoreModules::GetWorldEngine(); + if (engine) { + return engine->GetEntityVirtualWorld(_ent); + } + return 0; } void Entity::SetUpdateInterval(double interval) const { diff --git a/code/framework/src/world/client.cpp b/code/framework/src/world/client.cpp index b68382c6f..11afdd955 100644 --- a/code/framework/src/world/client.cpp +++ b/code/framework/src/world/client.cpp @@ -10,6 +10,7 @@ #include "game_rpc/set_frame.h" #include "game_rpc/set_transform.h" +#include "networking/messages/game_sync/entity_messages.h" namespace Framework::World { EngineError ClientEngine::Init() { @@ -82,11 +83,17 @@ namespace Framework::World { const auto rs = it.field(1); for (auto i : it) { - const auto &es = &rs[i]; const auto e = it.entity(i); - if (es->GetBaseEvents().updateProc && !e.has() && Framework::World::Engine::IsEntityOwner(e, myGUID.g)) { - es->GetBaseEvents().updateProc(_networkPeer, (SLNet::UNASSIGNED_RAKNET_GUID).g, e); + // Send updates for entities we own + if (!e.has() && Framework::World::Engine::IsEntityOwner(e, myGUID.g)) { + const auto sid = e.get(); + if (sid) { + Framework::Networking::Messages::GameSyncEntityUpdate entityUpdate; + entityUpdate.FromParameters(tr[i], 0); + entityUpdate.SetServerID(sid->id); + _networkPeer->Send(entityUpdate, (SLNet::UNASSIGNED_RAKNET_GUID).g); + } } } } @@ -109,8 +116,8 @@ namespace Framework::World { } } - if (str.modEvents.disconnectProc) { - str.modEvents.disconnectProc(e); + if (str.disconnectProc) { + str.disconnectProc(e); } e.destruct(); @@ -153,8 +160,8 @@ namespace Framework::World { *tr = rhs; const auto str = entity.get_mut(); - if (str->modEvents.updateTransformProc) { - str->modEvents.updateTransformProc(entity); + if (str && str->updateTransformProc) { + str->updateTransformProc(entity); } } } // namespace Framework::World diff --git a/code/framework/src/world/engine.cpp b/code/framework/src/world/engine.cpp index f91fdce42..196eccd29 100644 --- a/code/framework/src/world/engine.cpp +++ b/code/framework/src/world/engine.cpp @@ -90,4 +90,66 @@ namespace Framework::World { }); _world->defer_end(); } + + flecs::entity Engine::GetOrCreateVirtualWorld(int worldId) { + auto it = _virtualWorldCache.find(worldId); + if (it != _virtualWorldCache.end() && it->second.is_alive()) { + return it->second; + } + + // Create a new virtual world entity + std::string name = "VirtualWorld_" + std::to_string(worldId); + auto worldEntity = _world->entity(name.c_str()); + worldEntity.set({worldId}); + _virtualWorldCache[worldId] = worldEntity; + return worldEntity; + } + + void Engine::SetEntityVirtualWorld(flecs::entity e, int worldId) { + if (!e.is_valid() || !e.is_alive()) + return; + + // Remove from any existing virtual world + e.remove(flecs::Wildcard); + + // Add to new virtual world (world ID 0 means no world / default) + if (worldId != 0) { + auto worldEntity = GetOrCreateVirtualWorld(worldId); + e.add(worldEntity); + } + + // Also update the legacy field for backward compatibility + auto streamable = e.get_mut(); + if (streamable) { + streamable->virtualWorld = worldId; + } + } + + int Engine::GetEntityVirtualWorld(flecs::entity e) const { + if (!e.is_valid() || !e.is_alive()) + return 0; + + // Try to get from relation first + int worldId = 0; + e.each([&worldId](flecs::entity worldEntity) { + auto vw = worldEntity.get(); + if (vw) { + worldId = vw->id; + } + }); + + // Fallback to legacy field if no relation + if (worldId == 0) { + auto streamable = e.get(); + if (streamable) { + worldId = streamable->virtualWorld; + } + } + + return worldId; + } + + bool Engine::AreInSameVirtualWorld(flecs::entity a, flecs::entity b) const { + return GetEntityVirtualWorld(a) == GetEntityVirtualWorld(b); + } } // namespace Framework::World diff --git a/code/framework/src/world/engine.h b/code/framework/src/world/engine.h index f00a83b92..42b683f05 100644 --- a/code/framework/src/world/engine.h +++ b/code/framework/src/world/engine.h @@ -61,6 +61,9 @@ namespace Framework::World { // Cache for O(1) GUID lookups std::unordered_map _guidCache; + // Cache for virtual world entities (by ID) + std::unordered_map _virtualWorldCache; + public: EngineError Init(Networking::NetworkPeer *networkPeer); @@ -76,5 +79,11 @@ namespace Framework::World { flecs::world *GetWorld() const { return _world.get(); } + + // Virtual world management + flecs::entity GetOrCreateVirtualWorld(int worldId); + void SetEntityVirtualWorld(flecs::entity e, int worldId); + int GetEntityVirtualWorld(flecs::entity e) const; + bool AreInSameVirtualWorld(flecs::entity a, flecs::entity b) const; }; } // namespace Framework::World diff --git a/code/framework/src/world/modules/base.hpp b/code/framework/src/world/modules/base.hpp index ae221e15f..87871ae97 100644 --- a/code/framework/src/world/modules/base.hpp +++ b/code/framework/src/world/modules/base.hpp @@ -88,6 +88,23 @@ namespace Framework::World::Modules { // Usage: entity.add(streamerEntity) struct OwnedBy {}; + // Relation: Entity is currently being streamed to a streamer + // Usage: entity.add(streamerEntity) + // OnAdd triggers spawn RPC, OnRemove triggers despawn RPC + struct StreamedTo { + double lastUpdate = 0.0; + }; + + // Relation: Entity exists in a virtual world + // Usage: entity.add(worldEntity) + // Use Engine::GetOrCreateVirtualWorld(id) to get world entities + struct InVirtualWorld {}; + + // Tag: Marks an entity as a virtual world instance + struct VirtualWorld { + int id = 0; + }; + struct ServerID { flecs::entity_t id; }; @@ -109,23 +126,6 @@ namespace Framework::World::Modules { // Allows custom owner assignment logic, if method returns true we bypass framework's proximity based owner assignment AssignOwnerProc assignOwnerProc; - struct Events { - using Proc = fu2::function; - Proc spawnProc; - Proc despawnProc; - Proc selfUpdateProc; - Proc updateProc; - Proc ownerUpdateProc; - - // Events used locally for special needs - // These are NOT emitted through the network! - OnDisconnectProc disconnectProc; // called when the client disconnects from server - OnUpdateTransformProc updateTransformProc; // called whenever the server enforces a new transform upon the entity - }; - - // Extra set of events so mod can supply custom data. - Events modEvents; - // Custom visibility proc for additional visibility checks // Use VisibilityReplace tag to completely replace framework heuristics // Use VisibilityReplacePosition tag to replace only distance check @@ -136,37 +136,26 @@ namespace Framework::World::Modules { // If any of these entities are visible and ours is not, we force ours to be visible too. std::vector dependentEntities; - // Framework-level events. - friend Base; - - private: - Events events; - - public: - Events& GetBaseEvents() { - return events; - } - - [[maybe_unused]] Events& GetModEvents() { - return modEvents; - } + // Local lifecycle callbacks (not network events) + OnDisconnectProc disconnectProc; // called when the client disconnects from server + OnUpdateTransformProc updateTransformProc; // called whenever the server enforces a new transform upon the entity }; struct Streamer { using CollectRangeExemptEntities = fu2::function; - struct StreamData { - double lastUpdate = 0.0; - }; float range = 100.0f; uint64_t guid = 0xFFFFFFFFFFFFFFFF; uint16_t playerIndex = 0xFFFF; std::string nickname; std::string hardwareId; - std::unordered_map entities; std::unordered_set rangeExemptEntities; CollectRangeExemptEntities collectRangeExemptEntitiesProc; }; + // Prefab entities for efficient archetype instantiation + static inline flecs::entity StreamableEntityPrefab; + static inline flecs::entity StreamerEntityPrefab; + explicit Base(flecs::world &world) { world.module(); @@ -190,6 +179,21 @@ namespace Framework::World::Modules { world.component(); world.component(); world.component(); + world.component(); + world.component(); + world.component(); + + // Entity prefabs for efficient instantiation (LP-3) + // StreamableEntity: base components for any streamable entity + StreamableEntityPrefab = world.prefab("StreamableEntityPrefab") + .add() + .add() + .add(); + + // StreamerEntity: components for player/streamer entities + StreamerEntityPrefab = world.prefab("StreamerEntityPrefab") + .is_a(StreamableEntityPrefab) + .add(); // Windows bind metadata #ifdef _WIN32 @@ -206,8 +210,6 @@ namespace Framework::World::Modules { #endif } - static void SetupServerEmitters(Streamable& streamable); - static void SetupClientEmitters(Streamable& streamable); static void SetupServerReceivers(Framework::Networking::NetworkPeer *net, Framework::World::Engine *worldEngine); static void SetupClientReceivers(Framework::Networking::NetworkPeer *net, Framework::World::ClientEngine *worldEngine, Framework::World::Archetypes::StreamingFactory *streamingFactory); }; diff --git a/code/framework/src/world/modules/modules_impl.cpp b/code/framework/src/world/modules/modules_impl.cpp index b799460bd..d0f8c421c 100644 --- a/code/framework/src/world/modules/modules_impl.cpp +++ b/code/framework/src/world/modules/modules_impl.cpp @@ -16,82 +16,7 @@ #include "world/types/streaming.hpp" -#define CALL_CUSTOM_PROC(kind) \ - const auto streamable = e.get(); \ - if (streamable != nullptr) { \ - if (streamable->modEvents.kind != nullptr) { \ - streamable->modEvents.kind(peer, guid, e); \ - } \ - } - namespace Framework::World::Modules { - void Base::SetupServerEmitters(Streamable& streamable) { - streamable.events.spawnProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - Framework::Networking::Messages::GameSyncEntitySpawn entitySpawn; - const auto tr = e.get(); - if (tr) - entitySpawn.FromParameters(*tr); - entitySpawn.SetServerID(e.id()); - peer->Send(entitySpawn, guid); - CALL_CUSTOM_PROC(spawnProc); - return true; - }; - - streamable.events.despawnProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - CALL_CUSTOM_PROC(despawnProc); - Framework::Networking::Messages::GameSyncEntityDespawn entityDespawn; - entityDespawn.SetServerID(e.id()); - peer->Send(entityDespawn, guid); - return true; - }; - - streamable.events.selfUpdateProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - Framework::Networking::Messages::GameSyncEntitySelfUpdate entitySelfUpdate; - entitySelfUpdate.SetServerID(e.id()); - peer->Send(entitySelfUpdate, guid); - CALL_CUSTOM_PROC(selfUpdateProc); - return true; - }; - - streamable.events.updateProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - Framework::Networking::Messages::GameSyncEntityUpdate entityUpdate; - const auto tr = e.get(); - const auto es = e.get(); - if (tr && es) - entityUpdate.FromParameters(*tr, es->owner); - entityUpdate.SetServerID(e.id()); - peer->Send(entityUpdate, guid); - CALL_CUSTOM_PROC(updateProc); - return true; - }; - - streamable.events.ownerUpdateProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - Framework::Networking::Messages::GameSyncEntityOwnerUpdate entityUpdate; - const auto tr = e.get(); - const auto es = e.get(); - if (tr && es) - entityUpdate.FromParameters(es->owner); - entityUpdate.SetServerID(e.id()); - peer->Send(entityUpdate, guid); - CALL_CUSTOM_PROC(ownerUpdateProc); - return true; - }; - } - void Base::SetupClientEmitters(Streamable& streamable) { - streamable.events.updateProc = [&](Framework::Networking::NetworkPeer *peer, uint64_t guid, flecs::entity e) { - Framework::Networking::Messages::GameSyncEntityUpdate entityUpdate; - const auto tr = e.get(); - const auto sid = e.get(); - if (tr && sid) { - entityUpdate.FromParameters(*tr, 0); - entityUpdate.SetServerID(sid->id); - } - peer->Send(entityUpdate, guid); - CALL_CUSTOM_PROC(updateProc); - return true; - }; - } - void Base::SetupServerReceivers(Framework::Networking::NetworkPeer *net, Framework::World::Engine *worldEngine) { using namespace Framework::Networking::Messages; net->RegisterMessage(GameMessages::GAME_SYNC_ENTITY_UPDATE, [worldEngine](SLNet::RakNetGUID guid, GameSyncEntityUpdate *msg) { diff --git a/code/framework/src/world/server.cpp b/code/framework/src/world/server.cpp index 6c4a4746b..d5585feea 100644 --- a/code/framework/src/world/server.cpp +++ b/code/framework/src/world/server.cpp @@ -8,6 +8,7 @@ #include "server.h" +#include "networking/messages/game_sync/entity_messages.h" #include "utils/time.h" namespace Framework::World { @@ -20,6 +21,21 @@ namespace Framework::World { _findAllResourceEntities = _world->query_builder().build(); + // Specialized queries for decomposed visibility system (MP-7) + // AlwaysVisible entities skip distance checks entirely + _alwaysVisibleEntities = _world->query_builder() + .with() + .without() + .without() + .build(); + + // Normal streamable entities (excludes AlwaysVisible and Hidden) + _normalStreamableEntities = _world->query_builder() + .without() + .without() + .without() + .build(); + // Observer to sync OwnedBy relation to owner GUID for network messages _world->observer() .with(flecs::Wildcard) @@ -36,25 +52,19 @@ namespace Framework::World { } }); + // Note: StreamedTo relation changes trigger Flecs OnAdd/OnRemove events. + // Games should set up their own observers to handle spawn/despawn messages + // for specific entity types (e.g., Human, Vehicle). + // The framework only manages the relation based on visibility. + // Set up a system to remove entities we no longer need. _world->system("RemoveEntities").kind(flecs::PostUpdate).interval(cfg.removeEntitiesTickInterval).run([this](flecs::iter &it) { while (it.next()) { - auto streamables = it.field(1); - for (auto i : it) { auto e = it.entity(i); - auto &streamable = streamables[i]; - - // Remove the entity from all streamers. - _findAllStreamerEntities.each([this, &e, &streamable](flecs::entity rhsE, Modules::Base::Streamer &rhsS) { - if (rhsS.entities.find(e) != rhsS.entities.end()) { - rhsS.entities.erase(e); - // Ensure we despawn the entity from the client. - if (streamable.GetBaseEvents().despawnProc) - streamable.GetBaseEvents().despawnProc(_networkPeer, rhsS.guid, e); - } - }); + // Remove all StreamedTo relations (triggers despawn observers) + e.remove(flecs::Wildcard); e.destruct(); } @@ -143,6 +153,10 @@ namespace Framework::World { }); // Set up a system to stream entities to clients. + // This system manages StreamedTo relations based on visibility. + // Decomposed into phases for efficiency (MP-7): + // Phase 1: AlwaysVisible entities - always stream, skip distance check + // Phase 2: Normal entities - full visibility check _world->system("StreamEntities") .kind(flecs::PostUpdate) .interval(cfg.tickInterval) @@ -153,64 +167,49 @@ namespace Framework::World { const auto rs = it.field(2); for (auto i : it) { + auto streamerEntity = it.entity(i); + // Skip streamer entities we plan to remove. - if (it.entity(i).get() != nullptr) + if (streamerEntity.get() != nullptr) continue; - // Grab all streamable entities. - _allStreamableEntities.each([&](flecs::entity e, Modules::Base::Transform &otherTr, Modules::Base::Streamable &otherS) { - // Skip dead entities. - if (!e.is_alive()) + // Phase 1: AlwaysVisible entities - skip distance check, always visible + // Uses pre-filtered query (excludes PendingRemoval, Hidden) + // Note: AlwaysVisible is a zero-size tag, filtered by query but not passed to callback + _alwaysVisibleEntities.each([&](flecs::entity e, Modules::Base::Transform &otherTr, Modules::Base::Streamable &otherS) { + if (!e.is_alive() || e == streamerEntity) return; - // Let streamer send an update to self if an event is assigned. - if (e == it.entity(i) && rs[i].GetBaseEvents().selfUpdateProc && !e.has()) { - rs[i].GetBaseEvents().selfUpdateProc(_networkPeer, s[i].guid, e); + // Only check virtual world (skip distance/custom visibility) + if (!AreInSameVirtualWorld(streamerEntity, e)) return; - } - // Figure out entity visibility. - const auto id = e.id(); - const auto canSend = this->IsEntityVisibleToStreamer(it.entity(i), e, tr[i], s[i], rs[i], otherTr, otherS); - const auto map_it = s[i].entities.find(id); + const auto isStreaming = e.has(streamerEntity); + if (!isStreaming) { + Modules::Base::StreamedTo streamData; + streamData.lastUpdate = static_cast(Utils::Time::GetTime()); + e.set(streamerEntity, streamData); + } + }); - // Entity is already known to this streamer. - if (map_it != s[i].entities.end()) { - // If we can't stream an entity anymore, despawn it - if (!canSend) { - s[i].entities.erase(map_it); - if (otherS.GetBaseEvents().despawnProc) - otherS.GetBaseEvents().despawnProc(_networkPeer, s[i].guid, e); - } + // Phase 2: Normal entities - full visibility check + // Uses pre-filtered query (excludes PendingRemoval, Hidden, AlwaysVisible) + _normalStreamableEntities.each([&](flecs::entity e, Modules::Base::Transform &otherTr, Modules::Base::Streamable &otherS) { + if (!e.is_alive() || e == streamerEntity) + return; - // otherwise we do regular updates - else if (rs[i].owner != otherS.owner) { - auto &data = map_it->second; - if (static_cast(Utils::Time::GetTime()) - data.lastUpdate > otherS.updateInterval) { - if (otherS.GetBaseEvents().updateProc && !e.has()) - otherS.GetBaseEvents().updateProc(_networkPeer, s[i].guid, e); - data.lastUpdate = static_cast(Utils::Time::GetTime()); - } - } - else { - auto &data = map_it->second; - - // If the entity is owned by this streamer, we send a full update. - if (static_cast(Utils::Time::GetTime()) - data.lastUpdate > otherS.updateInterval) { - if (otherS.GetBaseEvents().ownerUpdateProc) - otherS.GetBaseEvents().ownerUpdateProc(_networkPeer, s[i].guid, e); - data.lastUpdate = static_cast(Utils::Time::GetTime()); - } - } - } + // Full visibility check for normal entities + const auto canSend = this->IsEntityVisibleToStreamer(streamerEntity, e, tr[i], s[i], rs[i], otherTr, otherS); + const auto isStreaming = e.has(streamerEntity); - // this is a new entity, spawn it unless user says otherwise - else if (canSend && otherS.GetBaseEvents().spawnProc) { - if (otherS.GetBaseEvents().spawnProc(_networkPeer, s[i].guid, e)) { - Modules::Base::Streamer::StreamData data; - data.lastUpdate = static_cast(Utils::Time::GetTime()); - s[i].entities[id] = data; + if (isStreaming) { + if (!canSend) { + e.remove(streamerEntity); } + } else if (canSend) { + Modules::Base::StreamedTo streamData; + streamData.lastUpdate = static_cast(Utils::Time::GetTime()); + e.set(streamerEntity, streamData); } }); } @@ -361,7 +360,7 @@ namespace Framework::World { return false; // Validate if the entity resides in the same virtual world client does. - if (lhsS.virtualWorld != rhsS.virtualWorld) + if (!AreInSameVirtualWorld(streamerEntity, e)) return false; // Let user replace the distance check. diff --git a/code/framework/src/world/server.h b/code/framework/src/world/server.h index 0c038fb7f..77ca3d4ed 100644 --- a/code/framework/src/world/server.h +++ b/code/framework/src/world/server.h @@ -58,6 +58,11 @@ namespace Framework::World { using IsVisibleProc = fu2::function; + // Specialized queries for decomposed visibility system (MP-7) + // AlwaysVisible is a zero-size tag, added via .with<>() in query builder + flecs::query _alwaysVisibleEntities; + flecs::query _normalStreamableEntities; + private: bool IsEntityVisibleToStreamerInternal(const flecs::entity streamerEntity, const flecs::entity e, const Modules::Base::Transform &lhsTr, const Modules::Base::Streamer &streamer, const Modules::Base::Streamable &lhsS, const Modules::Base::Transform &rhsTr, const Modules::Base::Streamable &rhsS, std::unordered_set &visited) const; diff --git a/code/framework/src/world/types/player.hpp b/code/framework/src/world/types/player.hpp index 21f590a3c..87712f50c 100644 --- a/code/framework/src/world/types/player.hpp +++ b/code/framework/src/world/types/player.hpp @@ -18,8 +18,12 @@ namespace Framework::World::Archetypes { class PlayerFactory { private: inline void SetupDefaults(flecs::entity e, uint64_t guid) { + // Inherit from streamer prefab for efficient component allocation (LP-3) + // Note: StreamerEntityPrefab already inherits StreamableEntityPrefab + e.is_a(World::Modules::Base::StreamerEntityPrefab); + auto &streamer = e.ensure(); - streamer.guid = guid; + streamer.guid = guid; } public: diff --git a/code/framework/src/world/types/streaming.hpp b/code/framework/src/world/types/streaming.hpp index 09875fc4d..29dc87db9 100644 --- a/code/framework/src/world/types/streaming.hpp +++ b/code/framework/src/world/types/streaming.hpp @@ -16,31 +16,22 @@ namespace Framework::World::Archetypes { class StreamingFactory { private: inline void SetupDefaults(flecs::entity e, uint64_t guid) { - e.add(); + // Inherit from prefab for efficient component allocation (LP-3) + e.is_a(Modules::Base::StreamableEntityPrefab); + // Configure instance-specific values auto &streamable = e.ensure(); streamable.owner = guid; streamable.defaultUpdateInterval = CoreModules::GetTickRate() * 1000.0f; // we need ms here - - e.add(); } public: inline void SetupClient(flecs::entity e, uint64_t guid) { SetupDefaults(e, guid); - - auto& streamable = e.ensure(); - Framework::World::Modules::Base::SetupClientEmitters(streamable); - - auto ass = e.get_mut(); - (void)ass; } inline void SetupServer(flecs::entity e, uint64_t guid) { SetupDefaults(e, guid); - - auto& streamable = e.ensure(); - Framework::World::Modules::Base::SetupServerEmitters(streamable); } }; } // namespace Framework::World::Archetypes From 8c4a7b46156562aa87bb2a20957071e7720e8b9c Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:28:24 +0100 Subject: [PATCH 3/8] Update server.cpp --- code/framework/src/world/server.cpp | 92 +++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/code/framework/src/world/server.cpp b/code/framework/src/world/server.cpp index d5585feea..2e18e7ac6 100644 --- a/code/framework/src/world/server.cpp +++ b/code/framework/src/world/server.cpp @@ -52,10 +52,48 @@ namespace Framework::World { } }); - // Note: StreamedTo relation changes trigger Flecs OnAdd/OnRemove events. - // Games should set up their own observers to handle spawn/despawn messages - // for specific entity types (e.g., Human, Vehicle). - // The framework only manages the relation based on visibility. + // Framework-level observer: Send spawn message when StreamedTo relation is added + _world->observer("SpawnObserver") + .with(flecs::Wildcard) + .event(flecs::OnSet) + .each([this](flecs::iter& it, size_t row) { + auto e = it.entity(row); + auto streamerEntity = it.pair(0).second(); + + if (!streamerEntity.is_valid() || !streamerEntity.is_alive()) + return; + + auto streamer = streamerEntity.get(); + if (!streamer) + return; + + auto tr = e.get(); + Networking::Messages::GameSyncEntitySpawn spawnMsg; + if (tr) + spawnMsg.FromParameters(*tr); + spawnMsg.SetServerID(e.id()); + _networkPeer->Send(spawnMsg, streamer->guid); + }); + + // Framework-level observer: Send despawn message when StreamedTo relation is removed + _world->observer("DespawnObserver") + .with(flecs::Wildcard) + .event(flecs::OnRemove) + .each([this](flecs::iter& it, size_t row) { + auto e = it.entity(row); + auto streamerEntity = it.pair(0).second(); + + if (!streamerEntity.is_valid() || !streamerEntity.is_alive()) + return; + + auto streamer = streamerEntity.get(); + if (!streamer) + return; + + Networking::Messages::GameSyncEntityDespawn despawnMsg; + despawnMsg.SetServerID(e.id()); + _networkPeer->Send(despawnMsg, streamer->guid); + }); // Set up a system to remove entities we no longer need. _world->system("RemoveEntities").kind(flecs::PostUpdate).interval(cfg.removeEntitiesTickInterval).run([this](flecs::iter &it) { @@ -173,6 +211,46 @@ namespace Framework::World { if (streamerEntity.get() != nullptr) continue; + // Helper lambda to handle entity updates for already-streaming entities + auto handleEntityUpdate = [&](flecs::entity e, Modules::Base::Transform &otherTr, Modules::Base::Streamable &otherS) { + // Skip entities that don't want tick updates + if (e.has()) + return; + + auto* streamData = e.get_mut(streamerEntity); + if (!streamData) + return; + + const double now = static_cast(Utils::Time::GetTime()); + if (now - streamData->lastUpdate < otherS.updateInterval) + return; + + streamData->lastUpdate = now; + + // Send update based on ownership + if (otherS.owner == s[i].guid) { + // Entity is owned by this streamer - send owner update + Networking::Messages::GameSyncEntityOwnerUpdate ownerUpdate; + ownerUpdate.FromParameters(otherS.owner); + ownerUpdate.SetServerID(e.id()); + _networkPeer->Send(ownerUpdate, s[i].guid); + } + else { + // Non-owner - send full transform update + Networking::Messages::GameSyncEntityUpdate entityUpdate; + entityUpdate.FromParameters(otherTr, otherS.owner); + entityUpdate.SetServerID(e.id()); + _networkPeer->Send(entityUpdate, s[i].guid); + } + }; + + // Send self-update for the streamer's own entity + if (!streamerEntity.has()) { + Networking::Messages::GameSyncEntitySelfUpdate selfUpdate; + selfUpdate.SetServerID(streamerEntity.id()); + _networkPeer->Send(selfUpdate, s[i].guid); + } + // Phase 1: AlwaysVisible entities - skip distance check, always visible // Uses pre-filtered query (excludes PendingRemoval, Hidden) // Note: AlwaysVisible is a zero-size tag, filtered by query but not passed to callback @@ -189,6 +267,9 @@ namespace Framework::World { Modules::Base::StreamedTo streamData; streamData.lastUpdate = static_cast(Utils::Time::GetTime()); e.set(streamerEntity, streamData); + } else { + // Already streaming - send update if interval passed + handleEntityUpdate(e, otherTr, otherS); } }); @@ -205,6 +286,9 @@ namespace Framework::World { if (isStreaming) { if (!canSend) { e.remove(streamerEntity); + } else { + // Still visible - send update if interval passed + handleEntityUpdate(e, otherTr, otherS); } } else if (canSend) { Modules::Base::StreamedTo streamData; From 7eb340bfefe1b8ba96cdd6ab9827e4602339796b Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:18:06 +0100 Subject: [PATCH 4/8] Fix cache initialization --- code/framework/src/world/client.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/code/framework/src/world/client.cpp b/code/framework/src/world/client.cpp index 11afdd955..b33c40bdd 100644 --- a/code/framework/src/world/client.cpp +++ b/code/framework/src/world/client.cpp @@ -69,6 +69,7 @@ namespace Framework::World { auto &sid = e.ensure(); sid.id = serverID; + e.modified(); return e; } From d69aa9e1e85e95d58bfdeafcc3b60a23653806e0 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:20:28 +0100 Subject: [PATCH 5/8] Reset owner GUID when ownership is cleared --- code/framework/src/world/server.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/code/framework/src/world/server.cpp b/code/framework/src/world/server.cpp index 2e18e7ac6..8fb29ff6b 100644 --- a/code/framework/src/world/server.cpp +++ b/code/framework/src/world/server.cpp @@ -339,9 +339,21 @@ namespace Framework::World { void ServerEngine::SetOwnerRelation(flecs::entity e, flecs::entity owner) { // Remove any existing ownership e.remove(flecs::Wildcard); - // Add new ownership + + // Get the streamable component to update the owner GUID + const auto streamable = e.get_mut(); + + // Add new ownership or clear the owner GUID if (owner.is_valid() && owner.is_alive()) { e.add(owner); + // Update the owner GUID from the streamer component + if (streamable) { + const auto streamer = owner.get(); + streamable->owner = streamer ? streamer->guid : 0; + } + } else if (streamable) { + // Clear the stale owner GUID when ownership is removed + streamable->owner = 0; } } From 13a618df638f5a3a3bbf9f69c36d33454fd74ad3 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:22:19 +0100 Subject: [PATCH 6/8] Sync updateInterval with the new default --- code/framework/src/world/types/streaming.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/code/framework/src/world/types/streaming.hpp b/code/framework/src/world/types/streaming.hpp index 29dc87db9..a12be32cc 100644 --- a/code/framework/src/world/types/streaming.hpp +++ b/code/framework/src/world/types/streaming.hpp @@ -23,6 +23,7 @@ namespace Framework::World::Archetypes { auto &streamable = e.ensure(); streamable.owner = guid; streamable.defaultUpdateInterval = CoreModules::GetTickRate() * 1000.0f; // we need ms here + streamable.updateInterval = streamable.defaultUpdateInterval; } public: From ebaa697238d4b38256981ee0dc9d39493f78252f Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:24:02 +0100 Subject: [PATCH 7/8] Missing OnRemove observer leaves Streamable.owner stale when ownership is cleared --- code/framework/src/world/server.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/code/framework/src/world/server.cpp b/code/framework/src/world/server.cpp index 8fb29ff6b..299b50cfb 100644 --- a/code/framework/src/world/server.cpp +++ b/code/framework/src/world/server.cpp @@ -52,6 +52,18 @@ namespace Framework::World { } }); + // Observer to clear owner GUID when OwnedBy relation is removed + _world->observer() + .with(flecs::Wildcard) + .event(flecs::OnRemove) + .each([](flecs::iter& it, size_t row) { + auto e = it.entity(row); + auto streamable = e.get_mut(); + if (streamable) { + streamable->owner = 0; + } + }); + // Framework-level observer: Send spawn message when StreamedTo relation is added _world->observer("SpawnObserver") .with(flecs::Wildcard) From 4d0304a7a2a99808564297d3b35e578249c9235a Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:25:00 +0100 Subject: [PATCH 8/8] Use flecs::OnAdd instead of flecs::OnSet for SpawnObserver --- code/framework/src/world/server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/framework/src/world/server.cpp b/code/framework/src/world/server.cpp index 299b50cfb..16a67fcc6 100644 --- a/code/framework/src/world/server.cpp +++ b/code/framework/src/world/server.cpp @@ -67,7 +67,7 @@ namespace Framework::World { // Framework-level observer: Send spawn message when StreamedTo relation is added _world->observer("SpawnObserver") .with(flecs::Wildcard) - .event(flecs::OnSet) + .event(flecs::OnAdd) .each([this](flecs::iter& it, size_t row) { auto e = it.entity(row); auto streamerEntity = it.pair(0).second();