Files
ledgrab/plans/game-integration/phase-1-event-bus.md
T
alexei.dolgolyov 492bdb95e3 feat: game integration system
Receive real-time events from games (CS2, Dota 2, LoL, etc.) and drive
LED effects through the existing color strip and value source pipelines.

Core:
- GameEventBus (thread-safe pub/sub) with standardized 23-type event vocabulary
- GameAdapter ABC + AdapterRegistry + MappingAdapter (YAML-driven)
- Built-in adapters: CS2 GSI, Dota 2 GSI, LoL Live Client, Generic Webhook
- Community YAML adapters: Minecraft, Valorant, Rocket League
- GameEventColorStripStream with 5 effects (flash/pulse/sweep/color_shift/breathing)
- GameEventValueSource with EMA smoothing and timeout
- 4 built-in effect presets (FPS Combat, MOBA Health, Racing, Generic Alert)
- Auto-setup for Valve GSI games (Steam path detection, cfg file writing)
- Demo capture engine exposed to non-demo mode

Frontend:
- Game tab in Streams tree navigation with integration cards
- Game integration editor modal with adapter picker, config fields, event mappings
- game_event source type in CSS and ValueSource editors
- Setup instructions overlay (markdown rendered)
- Live event monitor and connection test

API:
- Full CRUD for game integrations
- Event ingestion endpoint (adapter-level auth)
- Adapter metadata, presets, auto-setup, status/diagnostics endpoints
2026-03-31 13:17:52 +03:00

108 lines
8.0 KiB
Markdown

# Phase 1: Core Event Bus & Adapter Framework
**Status:** ✅ Complete
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Build the foundational event infrastructure: the standardized event model, in-process pub/sub bus, adapter ABC, adapter registry, and the YAML-driven mapping adapter. This phase creates the core abstractions that all subsequent phases depend on.
## Tasks
- [x] Task 1: Create `core/game_integration/__init__.py` package
- [x] Task 2: Define standardized event vocabulary as an enum/constant module (`core/game_integration/events.py`)
- Categories: health, armor, shield, mana, energy, ammo, gold, fuel, speed (continuous)
- Categories: kill, death, assist, damage_taken, damage_dealt (combat triggers)
- Categories: match_start, match_end, round_start, round_end (match flow triggers)
- Categories: objective_captured, objective_lost, objective_progress (objective)
- Categories: stunned, blinded, buffed, debuffed (status effects)
- Categories: team_a, team_b (team affiliation)
- Each event type has metadata: display name, category, value_type (continuous/trigger), default_range (min, max)
- [x] Task 3: Create `GameEvent` frozen dataclass (`core/game_integration/events.py`)
- Fields: `adapter_id`, `event_type` (str from vocabulary), `value` (float 0.0-1.0), `raw_data` (dict), `timestamp` (float, monotonic)
- [x] Task 4: Create `GameEventBus` (`core/game_integration/event_bus.py`)
- Thread-safe pub/sub with `threading.Lock`
- `publish(event: GameEvent)` — dispatches to matching subscribers
- `subscribe(event_type: str, callback)` → returns subscription ID
- `subscribe_all(callback)` → receives all events (for diagnostics/live monitor)
- `unsubscribe(subscription_id)`
- `get_recent_events(limit=50)` → returns recent events deque for diagnostics
- `get_stats()` → event counts per type, last event timestamp
- Use `collections.deque(maxlen=100)` for recent events
- [x] Task 5: Create `GameAdapter` ABC (`core/game_integration/base_adapter.py`)
- Class attributes: `ADAPTER_TYPE`, `DISPLAY_NAME`, `GAME_NAME`, `SUPPORTED_EVENTS` (list of event type strings)
- `parse_payload(cls, payload: dict, adapter_config: dict, prev_state: dict) -> tuple[list[GameEvent], dict]` — returns events + updated state (for diff detection)
- `validate_auth(cls, headers: dict, payload: dict, adapter_config: dict) -> bool`
- `get_config_schema(cls) -> dict` — returns JSON schema for adapter-specific config fields (for UI auto-generation)
- `get_setup_instructions(cls) -> str` — returns markdown setup guide for the game
- [x] Task 6: Create `AdapterRegistry` (`core/game_integration/adapter_registry.py`)
- `register(adapter_class)`, `get_adapter(adapter_type)`, `get_all_adapters()`, `get_available_adapters()`
- Follow `EngineRegistry` pattern
- [x] Task 7: Create `MappingAdapter` (`core/game_integration/mapping_adapter.py`)
- Parses community YAML adapter files into a generic adapter instance
- YAML schema: adapter name, game name, protocol (webhook/poll), mappings list
- Each mapping: `source_path` (JSONPath-like), `event` (standard type), `min`/`max` (for continuous), `trigger` mode (on_change/on_increase/on_decrease/on_value)
- `load_adapter_from_yaml(path) -> GameAdapter` — factory function
- `validate_adapter_yaml(data: dict) -> list[str]` — returns validation errors
- [x] Task 8: Create `core/game_integration/adapters/__init__.py` package (empty, for Phase 3)
- [x] Task 9: Write unit tests for GameEventBus (publish, subscribe, unsubscribe, thread safety)
- [x] Task 10: Write unit tests for AdapterRegistry (register, get, duplicates)
- [x] Task 11: Write unit tests for MappingAdapter (YAML parsing, payload translation, validation)
## Files to Modify/Create
- `server/src/wled_controller/core/game_integration/__init__.py` — package init, re-exports
- `server/src/wled_controller/core/game_integration/events.py` — GameEvent dataclass + event vocabulary
- `server/src/wled_controller/core/game_integration/event_bus.py` — GameEventBus
- `server/src/wled_controller/core/game_integration/base_adapter.py` — GameAdapter ABC
- `server/src/wled_controller/core/game_integration/adapter_registry.py` — AdapterRegistry
- `server/src/wled_controller/core/game_integration/mapping_adapter.py` — MappingAdapter + YAML loader
- `server/src/wled_controller/core/game_integration/adapters/__init__.py` — empty package
- `server/tests/core/test_game_event_bus.py` — EventBus tests
- `server/tests/core/test_adapter_registry.py` — Registry tests
- `server/tests/core/test_mapping_adapter.py` — MappingAdapter tests
## Acceptance Criteria
- GameEvent is frozen/immutable with all required fields
- Event vocabulary covers all standard categories with metadata
- EventBus correctly dispatches events to type-specific and wildcard subscribers
- EventBus is thread-safe (concurrent publish/subscribe doesn't crash)
- AdapterRegistry follows EngineRegistry pattern faithfully
- MappingAdapter can load a YAML file and translate a JSON payload into GameEvents
- MappingAdapter validates YAML schema and reports errors clearly
- All tests pass
## Notes
- Use `time.monotonic()` for timestamps (not `time.time()`) — monotonic is immune to clock adjustments
- Keep the event vocabulary extensible — new types should just be new entries, no code changes
- The MappingAdapter is key for scaling to many games without writing Python code per game
- `prev_state` parameter on `parse_payload` enables diff-based trigger detection (e.g. CS2 kills counter)
## Review Checklist
- [x] All tasks completed
- [x] Code follows project conventions (PEP 8, type annotations, immutability)
- [x] No unintended side effects
- [x] Tests pass (new + existing)
## Handoff to Next Phase
**Completed:** All 11 tasks implemented and tested. 55 tests pass, 0 failures. Ruff clean.
### What was built
- `events.py``GameEvent` frozen dataclass + 23-type event vocabulary with `EventTypeMetadata` (category, value_type, default_range). Helper functions: `get_event_vocabulary()`, `get_event_metadata()`, `is_known_event_type()`.
- `event_bus.py``GameEventBus` with thread-safe pub/sub. Type-specific and wildcard subscriptions, `deque(maxlen=100)` for recent events, per-type event counts. Callbacks invoked outside lock to prevent deadlocks.
- `base_adapter.py``GameAdapter` ABC with `ADAPTER_TYPE`/`DISPLAY_NAME`/`GAME_NAME`/`SUPPORTED_EVENTS` class vars. Abstract methods: `parse_payload()`, `validate_auth()`. Default implementations: `get_config_schema()`, `get_setup_instructions()`.
- `adapter_registry.py``AdapterRegistry` following `EngineRegistry` pattern (class-level dict, register/get/list/clear).
- `mapping_adapter.py``MappingAdapter` (concrete, instance-based) + `load_adapter_from_yaml()` factory + `validate_adapter_yaml()` validator. Supports dot-notation JSON paths, 4 trigger modes (on_change/on_increase/on_decrease/on_value), value normalization to 0.0-1.0, header-based auth.
- `adapters/__init__.py` — empty package ready for Phase 3 built-in adapters.
- `__init__.py` — re-exports all public API.
### Key design decisions
- `MappingAdapter` is instance-based (not classmethod-based like built-in adapters) because each YAML file creates a unique adapter with its own mappings. The `parse_payload`/`validate_auth` methods use `# type: ignore[override]` for the instance vs classmethod difference.
- `prev_state` dict keys are `source_path` strings, values are the last numeric value seen. This enables diff-based trigger detection (e.g., CS2 kill counter increments).
- Non-numeric values in payloads are treated as trigger events with value=1.0.
### What Phase 2 needs
- `GameEventBus` instance should be created as a singleton in `dependencies.py` and injected via FastAPI `Depends()`.
- `AdapterRegistry` is ready for built-in adapters to call `AdapterRegistry.register(MyAdapter)` at import time.
- The `GameIntegrationStore` (Phase 2) will need to store adapter configs that include `adapter_id` fields matching what `parse_payload` expects in `adapter_config`.