From f6969490ab81f3cefa2525b1d1474d9fa05d80c5 Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Thu, 15 Oct 2026 13:14:04 -0700 Subject: [PATCH 1/2] Snapshot component relation inputs before replacing inherited data --- CHANGELOG.md | 2 ++ tcod/ecs/entity.py | 3 ++- tests/test_relations.py | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1acd121..9eb0f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Replacing component relations from an inherited view now preserves values which depend on the destination. + - Removing a relation tag or component now preserves query membership when the other relation still exists. - Queries collecting component rows now resolve inherited components instead of raising `KeyError` for prefab instances. - Relation components can now be copied between distinct keys in the same entity, instead of the assignment being silently ignored. diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 0b206ff..c498c45 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -1071,9 +1071,10 @@ def __setitem__(self, __key: ComponentKey[T], __values: Mapping[Entity, T], /) - and __values.key == __key ): return + items = tuple(__values.items()) # The input may inherit values from the mapping being replaced. mapping: EntityComponentRelationMapping[T] = self[__key] mapping.clear() - for target, component in __values.items(): + for target, component in items: mapping[target] = component def __delitem__(self, key: ComponentKey[object]) -> None: diff --git a/tests/test_relations.py b/tests/test_relations.py index 451f5ee..364ca6a 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -359,3 +359,30 @@ def test_overlapping_relation_queries(key: ComponentKey[int], remove_first: str) else: actor.relation_tags_many[key].remove(faction) assert not any(queries) + + +@pytest.mark.parametrize("key", [int, ("affinity", int)]) +@pytest.mark.parametrize("depth", [1, 3]) +@pytest.mark.parametrize("override", [False, True]) +def test_component_relation_assignment_from_inherited_view( + key: ComponentKey[int], depth: int, *, override: bool +) -> None: + world = tcod.ecs.Registry() + prefab = world["prefab"] + first, second = world["first"], world["second"] + prefab.relation_components[key] = {first: 10, second: 20} + instance = prefab + for _ in range(depth): + instance = instance.instantiate() + if override: + instance.relation_components[key][first] = 30 + expected = {first: 30 if override else 10, second: 20} + query = world.Q.all_of(relations=[(key, second)]) + assert prefab in query + + prefab.relation_components[key] = instance.relation_components[key] + + assert dict(prefab.relation_components[key]) == expected + assert dict(instance.relation_components[key]) == expected + assert prefab in query + assert instance in query From a59138375db0df4d65a7daa439af8bfe5ce9cc1e Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Thu, 15 Oct 2026 13:45:01 -0700 Subject: [PATCH 2/2] Remove extra blank line from the fixed changelog entries --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb0f8f..d7eb0d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Replacing component relations from an inherited view now preserves values which depend on the destination. - - Removing a relation tag or component now preserves query membership when the other relation still exists. - Queries collecting component rows now resolve inherited components instead of raising `KeyError` for prefab instances. - Relation components can now be copied between distinct keys in the same entity, instead of the assignment being silently ignored.