diff --git a/CLAUDE.md b/CLAUDE.md index 2376b1630..83f8ccb08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,6 +71,25 @@ and an approval table in its `DocumentationTest`. Do not hand-roll a lookup tabl `NosCore.Data` being data-only and `NosCore.GameObject` holding the logic is deliberate. +### ECS layering + +The entity model is Arch components underneath, generated bundles on top — this split is +the decided design, not a migration in flight: + +- **Components** (`Ecs/Components/`) own ALL entity state. A new piece of per-entity + state goes into a component (or a new component), never into a bundle body, a service + dictionary keyed by entity, or a static. +- **Bundles** (`[ComponentBundle]` partial structs) are the generated facade the rest of + the code reads and writes. Hand-written bundle members are computed views only — + no backing fields. +- **Extension methods** (`Ecs/Extensions/`) are the per-entity behaviour layer; they act + on one entity through its bundle. +- **Systems** (`Ecs/Systems/`) are for iteration-heavy queries over many entities. + Prefer a system over LINQ across a bundle list when the call site runs per tick. +- **Hot paths do not materialise bundle lists.** `MapInstance.Monsters`/`Npcs` allocate a + fresh `List` per access; anything called from the map life loop enumerates the backing + dictionaries or a system query instead. + ### Deciding, in order 1. A **wire shape** — field order, separator, sentinel? → `NosCore.Packets`, evidenced by a diff --git a/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs b/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs index dbb63e283..a8ca469a8 100644 --- a/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs +++ b/src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs @@ -438,11 +438,11 @@ private async Task TickLifeAsync() return; } - foreach (var monster in Monsters) + foreach (var (_, monster) in _monsters) { await monster.TickLifeAsync(_monsterAi, _distanceCalculator, _clock, _logger); } - foreach (var npc in Npcs) + foreach (var (_, npc) in _npcs) { await npc.TickLifeAsync(_monsterAi, _distanceCalculator, _clock, _logger); } @@ -452,8 +452,8 @@ private async Task TickLifeAsync() // fine for buffs since their Duration is measured in deciseconds. if (_buffService != null) { - foreach (var monster in Monsters) await _buffService.TickAsync(monster).ConfigureAwait(false); - foreach (var npc in Npcs) await _buffService.TickAsync(npc).ConfigureAwait(false); + foreach (var (_, monster) in _monsters) await _buffService.TickAsync(monster).ConfigureAwait(false); + foreach (var (_, npc) in _npcs) await _buffService.TickAsync(npc).ConfigureAwait(false); foreach (var session in _sessionRegistry.GetClientSessionsByMapInstance(MapInstanceId)) { if (!session.HasPlayerEntity)