Implement per-entity KnowledgeGraph component with BTreeMap storage for deterministic iteration (D-010). StableEntityId + EntityRegistry for entity identity across save/load. KnowledgeEventQueue + processing system for decoupled knowledge updates. Decay system runs once per game-minute. All types conform to D-041 canonical structs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
//! Entity ID infrastructure (Q-019, D-041).
|
||||
//!
|
||||
//! StableEntityId component and EntityRegistry resource for
|
||||
//! bidirectional StableId <-> Entity mapping.
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::types::StableId;
|
||||
|
||||
/// Stable entity ID component. Assigned once at spawn, never changes.
|
||||
/// Serialized with entity for save/load.
|
||||
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct StableEntityId(pub StableId);
|
||||
|
||||
/// Bidirectional mapping between StableId and bevy Entity.
|
||||
/// Resource, updated on spawn/despawn.
|
||||
#[derive(Resource, Debug)]
|
||||
pub struct EntityRegistry {
|
||||
by_stable_id: BTreeMap<StableId, Entity>,
|
||||
by_entity: BTreeMap<Entity, StableId>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl Default for EntityRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityRegistry {
|
||||
pub fn new(seed_offset: u64) -> Self {
|
||||
Self {
|
||||
by_stable_id: BTreeMap::new(),
|
||||
by_entity: BTreeMap::new(),
|
||||
next_id: seed_offset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a new entity and assign a StableId.
|
||||
/// Returns the existing StableId if already registered.
|
||||
pub fn register(&mut self, entity: Entity) -> StableId {
|
||||
if let Some(id) = self.by_entity.get(&entity) {
|
||||
return *id;
|
||||
}
|
||||
let id = StableId(self.next_id);
|
||||
self.next_id += 1;
|
||||
self.by_stable_id.insert(id, entity);
|
||||
self.by_entity.insert(entity, id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Lookup: StableId -> Entity (for ECS queries).
|
||||
pub fn to_entity(&self, id: &StableId) -> Option<Entity> {
|
||||
self.by_stable_id.get(id).copied()
|
||||
}
|
||||
|
||||
/// Lookup: Entity -> StableId (for knowledge graph keys).
|
||||
pub fn to_stable(&self, entity: Entity) -> Option<StableId> {
|
||||
self.by_entity.get(&entity).copied()
|
||||
}
|
||||
|
||||
/// Remove a despawned entity from the registry.
|
||||
/// Called AFTER knowledge cleanup (tombstone pattern).
|
||||
pub fn unregister(&mut self, entity: Entity) {
|
||||
if let Some(id) = self.by_entity.remove(&entity) {
|
||||
self.by_stable_id.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of registered entities.
|
||||
pub fn len(&self) -> usize {
|
||||
self.by_entity.len()
|
||||
}
|
||||
|
||||
/// Whether the registry is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.by_entity.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bevy_ecs::world::World;
|
||||
|
||||
#[test]
|
||||
fn register_assigns_sequential_ids() {
|
||||
let mut world = World::new();
|
||||
let e1 = world.spawn_empty().id();
|
||||
let e2 = world.spawn_empty().id();
|
||||
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
let id1 = registry.register(e1);
|
||||
let id2 = registry.register(e2);
|
||||
|
||||
assert_eq!(id1, StableId(0));
|
||||
assert_eq!(id2, StableId(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_idempotent() {
|
||||
let mut world = World::new();
|
||||
let e1 = world.spawn_empty().id();
|
||||
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
let id_first = registry.register(e1);
|
||||
let id_second = registry.register(e1);
|
||||
|
||||
assert_eq!(id_first, id_second);
|
||||
assert_eq!(registry.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bidirectional_lookup() {
|
||||
let mut world = World::new();
|
||||
let e1 = world.spawn_empty().id();
|
||||
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
let id = registry.register(e1);
|
||||
|
||||
assert_eq!(registry.to_entity(&id), Some(e1));
|
||||
assert_eq!(registry.to_stable(e1), Some(id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unregister_removes_both_directions() {
|
||||
let mut world = World::new();
|
||||
let e1 = world.spawn_empty().id();
|
||||
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
let id = registry.register(e1);
|
||||
registry.unregister(e1);
|
||||
|
||||
assert_eq!(registry.to_entity(&id), None);
|
||||
assert_eq!(registry.to_stable(e1), None);
|
||||
assert!(registry.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_offset_starts_ids_higher() {
|
||||
let mut world = World::new();
|
||||
let e1 = world.spawn_empty().id();
|
||||
|
||||
let mut registry = EntityRegistry::new(1000);
|
||||
let id = registry.register(e1);
|
||||
|
||||
assert_eq!(id, StableId(1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_unknown_entity_returns_none() {
|
||||
let mut world = World::new();
|
||||
let e1 = world.spawn_empty().id();
|
||||
|
||||
let registry = EntityRegistry::new(0);
|
||||
assert_eq!(registry.to_entity(&StableId(999)), None);
|
||||
assert_eq!(registry.to_stable(e1), None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user