first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
# EntityPool
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`EntityPool` is the entity-ID allocator. It hands out positive integer IDs on `allocate()`, recycles freed IDs on `free()`, and tracks a per-ID generation counter so callers can detect stale references to a recycled slot. The pool keeps the live-ID range dense, which is important because every alive entity occupies a bitset slot and dense IDs limit the working-set footprint.
The pool is owned by `World`; the class is exported only so debug tooling can read its state.
## Features
- Recycle-first allocation: freed IDs are returned ahead of fresh ones.
- Per-ID generation counter — increments on every allocation, including reuse.
- O(1) `allocate`, `free`, `isAlive`, and `generation`.
- `peekNextId` / `setNextId` enable snapshot/restore of the underlying counter.
- `clear()` resets every counter and free-list back to the initial state.
- Defensive `setNextId` — rejects non-positive integers.
## Usage: Basic Example
```ts
import { EntityPool } from "@suaveplan/ecs";
const pool = new EntityPool();
const a = pool.allocate(); // 1
const b = pool.allocate(); // 2
pool.free(a);
const recycled = pool.allocate(); // 1 — freed id comes back first
```
## Usage: Advanced Example
```ts
import { EntityPool } from "@suaveplan/ecs";
const pool = new EntityPool();
const a = pool.allocate();
const generationAtSpawn = pool.generation(a); // 1
pool.free(a);
pool.allocate(); // recycles `a`
const generationNow = pool.generation(a); // 2
// External handle compares its captured generation against the current one
// to detect stale references to a slot that has since been recycled.
const handleIsStale = generationAtSpawn !== generationNow;
```
## API Reference
| Member | Description |
|---|---|
| `allocate()` | Return a fresh or recycled positive integer. Increments the entity's generation. |
| `free(entity)` | Move `entity` to the recycled list. Returns `false` for non-alive IDs. |
| `isAlive(entity)` | True when `entity` is currently allocated. |
| `generation(entity)` | Generation counter. Zero for never-allocated IDs. |
| `aliveCount` / `recycledCount` | Inspectable counts. |
| `peekNextId` | Next fresh ID that would be assigned. |
| `setNextId(id)` | Restore the counter from a snapshot. Rejects non-positive integers. |
| `getAlive()` | Read-only snapshot of the live set. |
| `clear()` | Reset every counter. |
## Implementation Notes
- The free-list is a plain `Entity[]` used as a LIFO stack. LIFO maximises cache reuse: the most recently freed slot is the first one re-handed, so it is still likely to be hot.
- The generation map is keyed by entity ID, not slot index, because IDs are themselves dense — using the ID as the key keeps the data structure simple.
- `peekNextId` returns the counter even after `clear()` (which resets it to `1`). The restore path uses this to rebuild the world's counter before re-spawning entities.
- The pool never reuses an ID that was returned by a fresh allocation but never freed — so an `aliveCount` of zero plus `recycledCount` of zero is enough to prove no stale handles exist.