I’ve been busy implementing synchronization of equipment for other players over the network. Meaning you’ll actually see someone switch their weapons/armor as they do it. During this implementation, my client’s began to crash. Turns out I was not using flecs entity Id creation correctly.
Nov 29th 2025 UPDATE
Turns out this didn’t work, I am getting lots of crashes and instability so I’ve reverted this patch. I now just have all entities on the client have a ServerEntityId field which I set once I get them from the server. Then I use custom flecs queries instead of World.Id(..server id..) to look them up. Not great, but I’m tired of fighting with this Id nonsense!
What I want to do
As a refresher, I want to have two sets of ranges of entity ids. One for the client to generate within a block of (0, 9000]. The client will manage these ids for things that don’t need to be tracked, such as other players equipment for visual representation. However a large number of entity id’s DO need to be tracked and synchronized with the server. In that case I want the server to have free range to generate id’s > 10000. Then send these entity ids down to the client so they know they are talking about the same thing.
The bug (or how I was mis-using an API)
I have been operating under the assumption this whole time that flecs ranges can be used to limit generation to just a particular range, BUT I would also be able to use manual Ids for Ids that come from the server.
For some reason this has worked quite well until now, when I started adding this new equipment feature due to the simple reason that I was never generating new Ids after I called world.make_alive(Server->EntityId). In this particular case I started getting asserts when I was updating my network player:
NewNetworkPlayer
.set<units::Attributes>({Attributes})
.set<units::TraitAttributes>({Traits})
.set<units::Velocity>({Vel})
.set<units::Vector3>({Pos})
.set<units::Quat>({Rot})
.is_a<items::Body>()
.add<items::Inventory>(
World.entity().add<items::Container>()
)
.set<character::NetworkCharacter>({PhysicsSystem, Pos, Rot, Entity->id()});
When that World.entity().add<..>() is called to pair the items::Inventory with the container, a range check assert kicks in and killed my process:
ecs_assert(!unsafe_world->info.max_id ||
ecs_entity_t_lo(entity) <= unsafe_world->info.max_id,
ECS_OUT_OF_RANGE, NULL);
How max_id is set
This max_id value is set whenever a new entity is created. Unfortunately, it seems to just increment after the last value that was ever created, meaning my make_alive(Server->EntityId) which, is again, purposely out of range, was setting this max_id to be higher than the allowed range that I set in the client.
What I want is to have this max_id not be set only when calling make_alive. I had few options to get this working:
- Manually creating an entity id manager and delegating the logic to it. This means I’d have to track every component creation and give it an Id. I’d also need to track when entities were destroyed. This sounds like a management nightmare and an easy way to introduce bugs.
- Patch flecs and add a new flag to the
flecs_entity_index_ensurefunction that, when set does NOT do:index->max_id = id > index->max_id ? id : index->max_id; - Patch flecs to just copy all of the make_alive functionality to a make_alive_untracked version that just doesn’t call that
index->max_id = id > index->max_id ? id : index->max_id;line.
Bullet no. 3 was by FAR the easiest to do, so I created a make_alive_untracked.patch that my build process will apply before building flecs.
Now in my CMakeLists.txt file instead of FetchContent_MakeAvailable(flecs) I split the fetch and build process into this:
# Adds flecs::
FetchContent_Declare(
flecs
GIT_REPOSITORY https://github.com/SanderMertens/flecs.git
GIT_TAG v4.1.2
)
# Handle flecs separately to apply patches
FetchContent_GetProperties(flecs)
if(NOT flecs_POPULATED)
FetchContent_Populate(flecs)
# Apply patch before building
find_package(Git REQUIRED)
execute_process(
COMMAND ${GIT_EXECUTABLE} apply --ignore-whitespace "${CMAKE_SOURCE_DIR}/third_party/flecs/patches/v4.1.2/make_alive_untracked.patch"
WORKING_DIRECTORY ${flecs_SOURCE_DIR}
RESULT_VARIABLE patch_result
OUTPUT_VARIABLE patch_output
ERROR_VARIABLE patch_error
)
if(NOT patch_result EQUAL 0)
message(WARNING "Failed to apply flecs patch: ${patch_error}")
else()
message(STATUS "Successfully applied flecs patch")
endif()
add_subdirectory(${flecs_SOURCE_DIR} ${flecs_BINARY_DIR})
endif()
This works in both windows and linux builds since I just use git to patch in that new function. Final step is to replace all make_alive(...) calls with my new make_alive_untracked(...) which doesn’t update that max id value.
I’m not sure this is worth pushing upstream (and the author of flecs agreed), so in the original GitHub issue I just provide my patch if anyone else wants to use it.
