492bdb95e3
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
8.0 KiB
8.0 KiB
Phase 1: Core Event Bus & Adapter Framework
Status: ✅ Complete Parent plan: 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
- Task 1: Create
core/game_integration/__init__.pypackage - 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)
- Task 3: Create
GameEventfrozen 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)
- Fields:
- Task 4: Create
GameEventBus(core/game_integration/event_bus.py)- Thread-safe pub/sub with
threading.Lock publish(event: GameEvent)— dispatches to matching subscriberssubscribe(event_type: str, callback)→ returns subscription IDsubscribe_all(callback)→ receives all events (for diagnostics/live monitor)unsubscribe(subscription_id)get_recent_events(limit=50)→ returns recent events deque for diagnosticsget_stats()→ event counts per type, last event timestamp- Use
collections.deque(maxlen=100)for recent events
- Thread-safe pub/sub with
- Task 5: Create
GameAdapterABC (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) -> boolget_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
- Class attributes:
- Task 6: Create
AdapterRegistry(core/game_integration/adapter_registry.py)register(adapter_class),get_adapter(adapter_type),get_all_adapters(),get_available_adapters()- Follow
EngineRegistrypattern
- 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),triggermode (on_change/on_increase/on_decrease/on_value) load_adapter_from_yaml(path) -> GameAdapter— factory functionvalidate_adapter_yaml(data: dict) -> list[str]— returns validation errors
- Task 8: Create
core/game_integration/adapters/__init__.pypackage (empty, for Phase 3) - Task 9: Write unit tests for GameEventBus (publish, subscribe, unsubscribe, thread safety)
- Task 10: Write unit tests for AdapterRegistry (register, get, duplicates)
- 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-exportsserver/src/wled_controller/core/game_integration/events.py— GameEvent dataclass + event vocabularyserver/src/wled_controller/core/game_integration/event_bus.py— GameEventBusserver/src/wled_controller/core/game_integration/base_adapter.py— GameAdapter ABCserver/src/wled_controller/core/game_integration/adapter_registry.py— AdapterRegistryserver/src/wled_controller/core/game_integration/mapping_adapter.py— MappingAdapter + YAML loaderserver/src/wled_controller/core/game_integration/adapters/__init__.py— empty packageserver/tests/core/test_game_event_bus.py— EventBus testsserver/tests/core/test_adapter_registry.py— Registry testsserver/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 (nottime.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_stateparameter onparse_payloadenables diff-based trigger detection (e.g. CS2 kills counter)
Review Checklist
- All tasks completed
- Code follows project conventions (PEP 8, type annotations, immutability)
- No unintended side effects
- 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—GameEventfrozen dataclass + 23-type event vocabulary withEventTypeMetadata(category, value_type, default_range). Helper functions:get_event_vocabulary(),get_event_metadata(),is_known_event_type().event_bus.py—GameEventBuswith 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—GameAdapterABC withADAPTER_TYPE/DISPLAY_NAME/GAME_NAME/SUPPORTED_EVENTSclass vars. Abstract methods:parse_payload(),validate_auth(). Default implementations:get_config_schema(),get_setup_instructions().adapter_registry.py—AdapterRegistryfollowingEngineRegistrypattern (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
MappingAdapteris instance-based (not classmethod-based like built-in adapters) because each YAML file creates a unique adapter with its own mappings. Theparse_payload/validate_authmethods use# type: ignore[override]for the instance vs classmethod difference.prev_statedict keys aresource_pathstrings, 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
GameEventBusinstance should be created as a singleton independencies.pyand injected via FastAPIDepends().AdapterRegistryis ready for built-in adapters to callAdapterRegistry.register(MyAdapter)at import time.- The
GameIntegrationStore(Phase 2) will need to store adapter configs that includeadapter_idfields matching whatparse_payloadexpects inadapter_config.