Skip to content

Extended usage of Flecs#181

Open
Segfaultd wants to merge 8 commits intodevelopfrom
experimental/ecs-improvements
Open

Extended usage of Flecs#181
Segfaultd wants to merge 8 commits intodevelopfrom
experimental/ecs-improvements

Conversation

@Segfaultd
Copy link
Member

@Segfaultd Segfaultd commented Jan 28, 2026

Summary

Modernize ECS usage by replacing legacy patterns with proper Flecs features: zero-size tags, observer-maintained caches, entity relations, and observer-based event handling.

Changes

Tags replace boolean fields:

  • Hidden tag replaces isVisible field (inverted logic)
  • AlwaysVisible tag replaces alwaysVisible field
  • NoTickUpdates tag replaces performTickUpdates field (inverted logic)
  • ManualOwnership tag replaces assignOwnerManually field
  • VisibilityReplace and VisibilityReplacePosition tags replace HeuristicMode enum

Zero-size marker components:

  • PendingRemoval and RemovedOnResourceReload converted to proper zero-size tags

O(1) entity lookups:

  • GUID→Entity cache with Flecs observers in Engine
  • ServerID→Entity cache with Flecs observers in ClientEngine
  • Virtual world entity cache for world lookups

Relations:

  • OwnedBy relation for entity-to-entity ownership modeling
  • StreamedTo relation tracks which entities are streamed to which players
  • InVirtualWorld relation for virtual world membership
  • Observer syncs OwnedBy relation to owner GUID field for network compatibility

Observer-based event system (replaces callback injection):

  • SpawnObserver: Sends GameSyncEntitySpawn when StreamedTo relation is added
  • DespawnObserver: Sends GameSyncEntityDespawn when StreamedTo relation is removed
  • Update messages sent in StreamEntities system based on ownership and intervals
  • Removed SetupServerEmitters() and SetupClientEmitters() - no longer needed
  • Removed Streamable.Events struct and all callback procs

Virtual world system:

  • VirtualWorld component marks world entities
  • InVirtualWorld relation for entity world membership
  • Helper methods: GetOrCreateVirtualWorld(), SetEntityVirtualWorld(), GetEntityVirtualWorld(), AreInSameVirtualWorld()

Entity prefabs:

  • StreamableEntityPrefab with Transform, Streamable, TickRateRegulator
  • StreamerEntityPrefab inheriting from StreamableEntityPrefab, adds Streamer
  • Factories now use e.is_a(Prefab) for efficient component allocation

Decomposed visibility system:

  • _alwaysVisibleEntities query: pre-filtered for AlwaysVisible entities (skip distance check)
  • _normalStreamableEntities query: pre-filtered for normal entities (full visibility check)
  • Query-level filtering via .with<>() and .without<>() instead of runtime checks

Breaking Changes

Removed from Streamable component:

  • bool isVisible → use Hidden tag
  • bool alwaysVisible → use AlwaysVisible tag
  • bool performTickUpdates → use NoTickUpdates tag
  • bool assignOwnerManually → use ManualOwnership tag
  • enum HeuristicMode and isVisibleHeuristic → use VisibilityReplace/VisibilityReplacePosition tags
  • Events struct (spawnProc, despawnProc, updateProc, etc.) → framework handles via observers
  • modEvents → use Flecs observers on StreamedTo relation for custom behavior

Removed from Streamer component:

  • std::unordered_map<flecs::entity_t, StreamData> entities → replaced by StreamedTo relation

Removed functions:

  • Modules::Base::SetupServerEmitters()
  • Modules::Base::SetupClientEmitters()

Migration Guide

// Visibility flags - Before
streamable.isVisible = false;
streamable.alwaysVisible = true;
streamable.performTickUpdates = false;
streamable.assignOwnerManually = true;
streamable.isVisibleHeuristic = HeuristicMode::REPLACE;

// Visibility flags - After
entity.add<Hidden>();
entity.add<AlwaysVisible>();
entity.add<NoTickUpdates>();
entity.add<ManualOwnership>();
entity.add<VisibilityReplace>();

// Virtual worlds - Before
streamable.virtualWorld = 5;

// Virtual worlds - After
engine->SetEntityVirtualWorld(entity, 5);

// Custom spawn/despawn handling - Before
streamable.modEvents.spawnProc = [](auto* peer, uint64_t guid, flecs::entity e) {
    // custom spawn logic
    return true;
};

// Custom spawn/despawn handling - After
// Set up observer on StreamedTo relation
world.observer("CustomSpawnHandler")
    .with<Modules::Base::StreamedTo>(flecs::Wildcard)
    .event(flecs::OnSet)
    .each([](flecs::iter& it, size_t row) {
        auto e = it.entity(row);
        auto streamer = it.pair(0).second();
        // custom spawn logic
    });

Benefits

  • Query-level filtering instead of runtime boolean checks
  • O(1) entity lookups instead of O(n) query scans
  • Proper ECS patterns with zero-size tags and relations
  • Reduced memory footprint in Streamable and Streamer components
  • Reactive event handling via Flecs observers instead of manual callback injection
  • Cleaner visibility logic using tag presence checks
  • Efficient prefabs for entity archetype instantiation

Test Plan

  • Framework builds successfully
  • MafiaMPServer builds successfully
  • Framework tests pass (183/184 - 1 pre-existing flaky test)
  • Scripting API updated (SetVisible, IsVisible, SetAlwaysVisible, SetVirtualWorld, etc.)

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Warning

Rate limit exceeded

@Segfaultd has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 2 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Visibility, ownership, and virtual-world handling moved from direct Streamable fields to ECS tag components and relation-based APIs; server/client GUID/ServerID caches added via observers; streaming/emitter logic refactored to use prefabs, iterator-based loops, and component-driven spawn/despawn semantics.

Changes

Cohort / File(s) Summary
Visibility & Scripting Builtins
code/framework/src/integrations/server/scripting/builtins/entity.cpp
Visibility APIs now add/remove Hidden / AlwaysVisible components; IsVisible/IsAlwaysVisible derive from component presence. Virtual-world getters/setters delegate to Engine with null checks; update interval still uses legacy Streamable.
Client — ServerID cache & messaging
code/framework/src/world/client.h, code/framework/src/world/client.cpp, code/framework/src/integrations/messages/entity_messages.h
Added _serverIdCache unordered_map + observers (ServerIDCacheUpdate/Remove). GetEntityByServerID uses cache. OnConnect entity tick now sends GameSyncEntityUpdate for owned entities; safer Streamable null checks.
Engine — GUID cache & virtual worlds
code/framework/src/world/engine.h, code/framework/src/world/engine.cpp
Added _guidCache and _virtualWorldCache with observers. New APIs: GetOrCreateVirtualWorld, SetEntityVirtualWorld, GetEntityVirtualWorld, AreInSameVirtualWorld. GetEntityByGUID uses cache; purge uses iterator-based traversal.
Server — ownership & streaming phases
code/framework/src/world/server.h, code/framework/src/world/server.cpp
Added SetOwnerRelation, GetOwnerRelation, IsOwnedBy. Streaming reworked into phased processing (AlwaysVisible vs Normal), uses tag components (Hidden, AlwaysVisible, NoTickUpdates, VisibilityReplace*) and OwnedBy relations; spawn/despawn driven by StreamedTo lifecycle; removal clears StreamedTo before destruction.
World modules & components (prefabs/tags)
code/framework/src/world/modules/base.hpp, code/framework/src/world/modules/modules_impl.cpp, code/framework/src/world/types/streaming.hpp, code/framework/src/world/types/player.hpp
Replaced Streamable booleans with tag components (Hidden, AlwaysVisible, NoTickUpdates, ManualOwnership, VisibilityReplace, VisibilityReplacePosition, OwnedBy, InVirtualWorld, VirtualWorld, StreamedTo). Added StreamableEntityPrefab / StreamerEntityPrefab. Removed emitter setup functions and emitter-based custom procs; streaming setup now uses prefabs and instance config.

Sequence Diagram(s)

sequenceDiagram
    participant Server as ServerEngine
    participant World as Engine
    participant Client as ClientEngine
    participant Net as Network

    Server->>World: Query entities (AlwaysVisible / Normal)
    World-->>Server: Return entities + VirtualWorld / OwnedBy relations
    Server->>Server: Evaluate visibility (Hidden/AlwaysVisible/VirtualWorld/Distance/Replace tags)
    Server->>World: Add/Remove `StreamedTo` relations (spawn/despawn)
    Server->>Net: Send GameSyncEntityUpdate (spawn/update/despawn)
    Net->>Client: Deliver GameSyncEntityUpdate
    Client->>Client: Lookup entity via _serverIdCache
    Client->>World: Request virtual-world or entity resolution if needed
    World-->>Client: Provide virtual-world/entity info
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • zpl-zak

Poem

🐰
Through tags I hop on nimble feet,
Hidden burrows and AlwaysVisible meet,
Owners tethered, worlds assigned,
Caches hum and lookups find,
Prefab fields weave streams complete. ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Extended usage of Flecs' is partially related to the changeset but is vague and overly broad. It refers to a real aspect of the change (increased Flecs usage), but fails to capture the main point: modernizing the ECS by replacing legacy boolean fields and linear lookups with tags, observers, and caches. Consider a more specific title such as 'Modernize ECS with Flecs tags and observer-maintained caches' or 'Replace legacy Streamable booleans with Flecs tags and relations' to better reflect the core architectural improvements.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/framework/src/world/server.cpp (1)

186-203: Apply NoTickUpdates to owner updates too.
The tag is documented as disabling tick updates, but the owner-update path still fires, so owned entities will still send updates when tagged.

🛠️ Suggested fix
-                                    if (otherS.GetBaseEvents().ownerUpdateProc)
+                                    if (otherS.GetBaseEvents().ownerUpdateProc && !e.has<Modules::Base::NoTickUpdates>())
                                         otherS.GetBaseEvents().ownerUpdateProc(_networkPeer, s[i].guid, e);
🤖 Fix all issues with AI agents
In `@code/framework/src/world/client.cpp`:
- Around line 24-35: The cache population fails because CreateEntity uses
e.ensure<Modules::Base::ServerID>() and then writes sid.id directly, which
doesn't fire the OnSet observer registered in ServerIDCacheUpdate; after
assigning sid.id = serverID in CreateEntity, call
e.modified<Modules::Base::ServerID>() to trigger the observer so _serverIdCache
is updated (this ensures GetEntityByServerID() can find new entities); keep the
existing ServerIDCacheUpdate and ServerIDCacheRemove observers unchanged.

In `@code/framework/src/world/server.cpp`:
- Around line 256-275: SetOwnerRelation currently removes the OwnedBy relation
but doesn't clear the stored owner GUID on the component, leaving
Streamable.owner stale and GetOwner() able to resolve the old owner; update
ServerEngine::SetOwnerRelation so that when owner is invalid or ownership is
being removed it also resets/clears the Streamable::owner GUID (or related owner
field) on entity e (use the same path you use to set it when adding ownership),
ensuring GetOwnerRelation/GetOwner return null after removal; touch the same
symbols Modules::Base::OwnedBy, ServerEngine::SetOwnerRelation, and
Streamable::owner (or the component that stores the owner GUID) so the GUID is
cleared when e.remove<Modules::Base::OwnedBy>(...) is called.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@code/framework/src/world/types/streaming.hpp`:
- Around line 18-25: The SetupDefaults function updates
streamable.defaultUpdateInterval but leaves streamable.updateInterval stale; in
SetupDefaults (function SetupDefaults, struct
Framework::World::Modules::Base::Streamable) set streamable.updateInterval =
streamable.defaultUpdateInterval (ensure units are ms as done for
defaultUpdateInterval) so newly created entities inherit the current tick-rate
cadence rather than the prefab's old value.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@code/framework/src/world/server.cpp`:
- Around line 39-53: The OnAdd observer that handles Modules::Base::OwnedBy
updates Streamable->owner but there is no OnRemove counterpart, so
Streamable.owner remains stale when ownership is cleared; add an OnRemove
observer on _world->observer() with Modules::Base::OwnedBy (flecs::Wildcard) and
in its each lambda locate the entity (same e),
get_mut<Modules::Base::Streamable>() and if present clear streamable->owner
(e.g. set to invalid/zero GUID) whenever the owner relation is removed (covers
removals from SetOwnerRelation and direct removals) so GetOwner() no longer
returns stale GUIDs.
- Around line 55-76: The SpawnObserver currently listens for flecs::OnSet which
fires on both additions and modifications; change the observer for
"SpawnObserver" that uses
.with<Modules::Base::StreamedTo>(flecs::Wildcard).event(flecs::OnSet) to use
.event(flecs::OnAdd) instead so spawn RPCs are only sent when the StreamedTo
relation is added (leave the rest of the lambda body—entity lookup, streamer
retrieval, Transform handling, SetServerID and Send—unchanged).
🧹 Nitpick comments (1)
code/framework/src/world/server.cpp (1)

112-137: Consider whether SetOwnerRelation should be used here for consistency.

The system directly sets streamable.owner without establishing the OwnedBy relation. This means entities assigned ownership through this automatic system won't have the relation-based ownership that SetOwnerRelation provides.

This may be intentional (keeping automatic GUID-based ownership separate from explicit relation-based ownership), but worth confirming the design intent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant