Metadata-Version: 2.5
Name: ecs-cub
Version: 0.1.7
Summary: An Entity Component System (ECS) package for Python with a C++23 core.
Author-email: chaz <bright.lid5647@fastmail.com>
Requires-Python: >=3.15
Requires-Dist: fond>=0.3.9
Description-Content-Type: text/markdown

# ecs-cub

A high-performance Entity Component System with a C++23 core and Python bindings via pybind11.

> ⚠️ Research project — not production ready. We're exploring, not shipping.

---

## Features

- **Entities** — lightweight integer IDs with freelist reuse and optional generation-checked `Entity` handles
- **Components** — works seamlessly with `@dataclass(slots=True)`, optional `on_added`/`on_removed` lifecycle hooks
- **Queries** — reusable query signatures, exclusion filters (`Exclude(Dead)`), and `NOT_ANY(A, B, C)` NOR semantics
- **Systems** — push model (ECS calls your `update()`) or pull model (you ask for results when you want them); opt out of entity IDs when you don't need them
- **Tags** — unique 1:1 string → entity mapping, O(1) lookup
- **Groups** — 1:many grouping, entities can belong to multiple groups simultaneously
- **Events** — signals (1:1 handler) and pub/sub (1:many subscribers); internal lifecycle events auto-published for entity/component changes
- **Resources** — Python-owned registry resources plus explicitly process-wide resources
- **Deferred kills** — `kill_entity()` is deferred by default to prevent iterator invalidation during system updates
- **Marker components** — filter by presence without passing the component into results, saving allocations per entity

---

## Performance

- **65MB** memory for 1,000,000 entities
- O(1) entity creation, component add/remove, tag and group lookups
- 32-bit bitmask signatures for fast system matching
- Sparse sets for component storage

---

## Features

### Entities

```python
from ecs_cub.api import Health, Inventory, Position, Velocity, create_entity, create_entity_obj

# Lightweight integer IDs
entity_id = create_entity(Position(0, 0), Velocity(1, 0))

# Or a generation-checked wrapper that cannot alias a recycled entity ID
player = create_entity_obj(Position(0, 0), Health(100))
player.tag("player")
player.add_component(Inventory())
player.kill()
```

IDs are reused via a freelist (LIFO). Max 1,048,576 entities by default, configurable at compile time.

---

### Components

```python
from dataclasses import dataclass
from ecs_cub.api import Component

@dataclass(slots=True)
class Position(Component):
    x: float
    y: float
```

Works seamlessly with `@dataclass(slots=True)`. Optional duck-typed lifecycle hooks:

```python
def on_added(self, entity_id): ...
def on_removed(self, entity_id): ...
```

---

### Queries

```python
from ecs_cub.api import Exclude, create_query, get_components

query = create_query(Position, Velocity)
for entity_id, (pos, vel) in get_components(query):
    pos.x += vel.dx

# Exclusion — entities with Position but NOT Dead
query = create_query(Position, Exclude(Dead))
```

---

### Systems

Two patterns, pick what fits:

```python
# Push model — ECS calls update() for you
class MovementSystem(SystemBase):
    required_components = (Position, Velocity)

    def update(self, results, dt):
        for entity_id, (pos, vel) in results:
            pos.x += vel.dx * dt

# Pull model — you ask for results when you want them
sys_idx = register_system(Position, Velocity)
results = get_system_results(sys_idx)
```

Skip entity IDs when you don't need them:

```python
class DecaySystem(SystemBase):
    required_components = (Health,)
    include_entity_id = False  # Skips Py_INCREF per entity — faster
```

---

### Tags & Groups

```python
# Tags — 1:1 unique mapping
tag_entity(entity_id, "player")
player_id = get_entity_by_tag("player")

# Groups — 1:many, entities can belong to multiple groups
group_entity(entity_id, "enemies")
group_entity(entity_id, "bosses")  # same entity, two groups
enemies = get_entities_by_group("enemies")
```

---

### Events & Pub/Sub

```python
# Signals — 1:1 handler
connect("player_died", on_player_death)
fire("player_died", player_id)

# Pub/Sub — 1:many subscribers
subscribe("damage_dealt", log_damage)
subscribe("damage_dealt", update_ui)
publish("damage_dealt", entity_id, amount)
```

Internal lifecycle events are auto-published on `_entity:created`, `_entity:killed`, `_component:added`, and `_component:removed`.

---

### Resources

Resources are indexed by their exact Python type. Registry-scoped resources stay in Python, so ordinary access does not
cross the C++ boundary.

```python
from dataclasses import dataclass

from ecs_cub.api import get_local_resource, set_resource


@dataclass(slots=True)
class FrameClock:
    delta_time: float = 0.0


clock = FrameClock(delta_time=0.016)
set_resource(clock)

# Lookups return MaybeIDK; value() raises when the resource is absent.
dt = get_local_resource(FrameClock).value().delta_time
```

There is one resource slot per exact type, so setting another `FrameClock` replaces the existing value. Passing a class
instead of an instance default-constructs it with no arguments.

Process-wide resources survive `clear_database()` for things like input state or configuration:

```python
from ecs_cub.api import get_global_resource, set_global_resource


@dataclass(slots=True)
class InputState:
    jump: bool = False


set_global_resource(InputState)
input_state = get_global_resource(InputState).value()
```

---

### Deferred Kills

```python
kill_entity(entity_id)                  # Deferred — safe during system iteration
kill_entity(entity_id, immediate=True)  # Immediate — use outside systems
```

---

## Installation

```bash
uv sync
mask clean_and_build
```

Requires Python 3.15+ and a C++23 compiler.

## Native C++ build

The public C++ API lives under `src/ecs_cub/include` and is consumed as, for example,
`#include <ecs_cub/registry.hpp>`. CMake consumers that add this repository as a subdirectory can link the
`ecs_cub::ecs_cub` target.

An installed package exposes the same native paths through Python:

```python
import ecs_cub

ecs_cub.get_include()      # Public C++ include directories
ecs_cub.get_libs()         # Built Python extension files
ecs_cub.get_cmake_dir()    # Directory containing EcsCubTarget.cmake
```

The CLI prints shell-friendly equivalents:

```bash
ecs-cub includes           # -I/path/to/ecs_cub/include
ecs-cub includes --path-only
ecs-cub libs               # One built extension path per line
ecs-cub cmake-dir
```

To consume an installed wheel from CMake:

```cmake
execute_process(
    COMMAND "${Python_EXECUTABLE}" -c "import ecs_cub; print(ecs_cub.get_cmake_dir())"
    OUTPUT_VARIABLE ECS_CUB_CMAKE_DIR
    OUTPUT_STRIP_TRAILING_WHITESPACE
    COMMAND_ERROR_IS_FATAL ANY
)
include("${ECS_CUB_CMAKE_DIR}/EcsCubTarget.cmake")
target_link_libraries(app PRIVATE ecs_cub::ecs_cub)
```

For a standalone Clang build against the sibling Fond checkout:

```bash
cmake -S . -B build-cmake \
    -DCMAKE_CXX_COMPILER=clang++
cmake --build build-cmake --parallel
ctest --test-dir build-cmake --output-on-failure
```

Set `FOND_ROOT` when Fond is somewhere other than `../fond`. The CMake-built Python extension is written to
`<build>/python/ecs_cub/api`; pass `-DECS_CUB_BUILD_PYTHON_MODULE=OFF` when only the native library is needed. Wheel builds
place their extension behind the same `ecs_cub.api` front door.

Enable diagnostic logging and strict Python callback errors with
`-DECS_CUB_DEBUG_MODE=ON -DECS_CUB_VERBOSE_LOGGING=ON -DECS_CUB_STRICT=ON`. The equivalent wheel workflow is
`mask debug_build`.

The native shutdown latch can be polled without running application logic inside a signal handler:

```python
import signal

from ecs_cub.api import install_shutdown_signal, restore_shutdown_signal, shutdown_requested


install_shutdown_signal(signal.SIGTERM)
try:
    while not shutdown_requested():
        run_frame()
finally:
    restore_shutdown_signal(signal.SIGTERM)
```

`consume_shutdown_request()` atomically reads and clears the latch. `signal_shutdown` is also compatible with
`signal.signal()` when Python-managed signal dispatch is preferred.

The GLFW/OpenGL map editor is an opt-in CMake target:

```bash
mask build map-editor
mask map-editor
```

Its executable lives at `build-cmake/bin/map_editor`; it runs with `tools/map_editor` as its working directory so the
existing maps, icons, and sample-game asset paths remain stable.
