Skip to content

Adding a component to an instantiated prefab aborts with ECS_ALREADY_IN_USE, which blocks live prefab editing. #2284

Description

@Wolftein

Describe the bug

Since ddb83fe (#2239, "Throw ALREADY_IN_USE error when changing components of an
instantiated prefab", v4.1.6), adding a component to a prefab that has at least one
instance aborts with ECS_ALREADY_IN_USE.

This makes live prefab editing impossible, which I think affects any editor built on
flecs rather than just mine. An editor almost always keeps a live instance of the prefab
the user is editing — a placement preview that follows the cursor, or a stand-in in a
preview/thumbnail viewport. So "the prefab has been instantiated" is the normal state
while editing, not an edge case, and "add a component to this prefab" is the core
editing operation. In my editor, creating a brand new archetype and adding any component
to it aborts, because the preview instance already exists.

Two things make it hard to work around:

  1. The check counts components the user never named. flecs_prefab_diff_has_override
    (src/addons/prefab/tree_spawner.c:378) treats a component as an auto-override when it
    has neither EcsIdOnInstantiateInherit nor EcsIdOnInstantiateDontInherit, i.e. the
    default. Because the whole table diff is inspected, adding a component that is
    explicitly (OnInstantiate, Inherit) — and therefore exempt — still aborts if it has an
    EcsWith companion left on the default. That is the case in the repro below.

  2. The obvious remedy is not available. The components being dragged in are derived
    runtime state (a world transform, a bounding volume) that every instance must own its
    own copy of, which is exactly why they are left on the default. Marking them
    DontInherit means instances no longer receive them at all
    (src/addons/prefab/instantiate.c:103 and :180); marking them Inherit means instances
    see the prefab's copy but do not own one, so nothing can write per-instance values.
    Either choice breaks the reason the component exists.

The assert is FLECS_DEBUG-only (src/addons/prefab/prefab.h:26-37), so release builds are
unaffected. The underlying behaviour is well defined — existing instances simply do not
receive the new component — and my application already propagates the change to instances
by hand, which is the strategy the assert now forbids.

To Reproduce

#include <flecs.h>

typedef struct { float x, y; } Position;
typedef struct { float x, y; } WorldPosition;

int main(void) {
    ecs_world_t *world = ecs_init();

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, WorldPosition);

    /* Position is authored, so instances inherit it: exempt from the new check. */
    ecs_add_pair(world, ecs_id(Position), EcsOnInstantiate, EcsInherit);

    /* WorldPosition is derived every frame, so every instance must own its own copy.
     * It is therefore left on the default OnInstantiate, and it is added automatically
     * wherever Position is added. */
    ecs_add_pair(world, ecs_id(Position), EcsWith, ecs_id(WorldPosition));

    ecs_entity_t prefab = ecs_entity(world, {
        .name = "Prefab", .add = ecs_ids(EcsPrefab) });

    /* An editor stands a live instance of the prefab so the user can see it. */
    ecs_entity_t preview = ecs_new_w_pair(world, EcsIsA, prefab);
    (void)preview;

    /* The user adds a component to the prefab in the inspector. */
    ecs_add(world, prefab, Position);   /* ECS_ALREADY_IN_USE */

    ecs_fini(world);
}

Adding a plain default-trait component directly (ecs_add(world, prefab, WorldPosition))
aborts the same way, without the EcsWith indirection.

Expected behavior

Adding a component to an instantiated prefab succeeds, leaving it to the application to
propagate the change to existing instances if it wants to (which is what an editor is
already doing).

Failing that, any of these would unblock the use case:

  • an opt-out, so an application that manages propagation itself can disable the check
    (a world flag, or a tag on the prefab);
  • not counting components pulled in by EcsWith, so that explicitly tagging a component
    (OnInstantiate, Inherit) is actually sufficient to make an add legal;
  • narrowing the assert to the cases that genuinely corrupt state, rather than every
    component left on the default trait.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions