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
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
<label data-i18n="automations.enabled">Enabled:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.enabled.hint">Disabled automations won't activate even when conditions are met</small>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.enabled.hint">Disabled automations won't activate even when rules are met</small>
|
||||
<label class="settings-toggle">
|
||||
<input type="checkbox" id="automation-editor-enabled" checked>
|
||||
<span class="settings-toggle-slider"></span>
|
||||
@@ -33,25 +33,25 @@
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="automation-editor-logic" data-i18n="automations.condition_logic">Condition Logic:</label>
|
||||
<label for="automation-editor-logic" data-i18n="automations.rule_logic">Rule Logic:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.condition_logic.hint">How multiple conditions are combined: ANY (OR) or ALL (AND)</small>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.rule_logic.hint">How multiple rules are combined: ANY (OR) or ALL (AND)</small>
|
||||
<select id="automation-editor-logic">
|
||||
<option value="or" data-i18n="automations.condition_logic.or">Any condition (OR)</option>
|
||||
<option value="and" data-i18n="automations.condition_logic.and">All conditions (AND)</option>
|
||||
<option value="or" data-i18n="automations.rule_logic.or">Any rule (OR)</option>
|
||||
<option value="and" data-i18n="automations.rule_logic.and">All rules (AND)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="automations.conditions">Conditions:</label>
|
||||
<label data-i18n="automations.rules">Rules:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.conditions.hint">Rules that determine when this automation activates</small>
|
||||
<div id="automation-conditions-list"></div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="addAutomationCondition()" style="margin-top: 6px;">
|
||||
+ <span data-i18n="automations.conditions.add">Add Condition</span>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.rules.hint">Rules that determine when this automation activates</small>
|
||||
<div id="automation-rules-list"></div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="addAutomationRule()" style="margin-top: 6px;">
|
||||
+ <span data-i18n="automations.rules.add">Add Rule</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<label for="automation-scene-id" data-i18n="automations.scene">Scene:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.scene.hint">Scene preset to activate when conditions are met</small>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.scene.hint">Scene preset to activate when rules are met</small>
|
||||
<select id="automation-scene-id"></select>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<label for="automation-deactivation-mode" data-i18n="automations.deactivation_mode">Deactivation:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.deactivation_mode.hint">What happens when conditions stop matching</small>
|
||||
<small class="input-hint" style="display:none" data-i18n="automations.deactivation_mode.hint">What happens when rules stop matching</small>
|
||||
<select id="automation-deactivation-mode">
|
||||
<option value="none" data-i18n="automations.deactivation_mode.none">None — keep current state</option>
|
||||
<option value="revert" data-i18n="automations.deactivation_mode.revert">Revert to previous state</option>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<option value="weather" data-i18n="color_strip.type.weather">Weather</option>
|
||||
<option value="processed" data-i18n="color_strip.type.processed">Processed</option>
|
||||
<option value="key_colors" data-i18n="color_strip.type.key_colors">Key Colors</option>
|
||||
<option value="game_event" data-i18n="color_strip.type.game_event">Game Event</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -689,6 +690,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Event section -->
|
||||
<div id="css-editor-game-event-section" style="display:none">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-game-integration" data-i18n="color_strip.game_event.integration">Game Integration:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.game_event.integration.hint">Select the game integration that provides events for this source.</small>
|
||||
<select id="css-editor-game-integration">
|
||||
<option value="">—</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="color_strip.game_event.idle_color">Idle Color:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.game_event.idle_color.hint">LED color when no game events are active.</small>
|
||||
<div id="css-editor-game-event-idle-color-container"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="color_strip.game_event.event_mappings">Event Mappings:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.game_event.event_mappings.hint">Override or add event-to-effect mappings for this source. These supplement the integration-level mappings.</small>
|
||||
<div class="gi-mapping-preset-row">
|
||||
<select id="css-editor-ge-mapping-preset" onchange="onCSSGameMappingPresetChange()">
|
||||
<option value="" data-i18n="game_integration.preset.select">Load preset...</option>
|
||||
<option value="fps_combat" data-i18n="game_integration.preset.fps_combat">FPS Combat</option>
|
||||
<option value="moba_health" data-i18n="game_integration.preset.moba_health">MOBA Health</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="css-editor-ge-mappings-list" class="gi-mappings-container"></div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="addCSSGameMapping()" style="margin-top:6px">
|
||||
<span data-i18n="game_integration.mapping.add">+ Add Mapping</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared LED count field -->
|
||||
<div id="css-editor-led-count-group" class="form-group">
|
||||
<div class="label-row">
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<div id="game-integration-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="gi-title">
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h2 id="gi-title" data-i18n="game_integration.add">Add Game Integration</h2>
|
||||
<button class="modal-close-btn" onclick="closeGameIntegrationModal()" data-i18n-aria-label="aria.close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="gi-id">
|
||||
<div id="gi-error" class="modal-error" style="display:none"></div>
|
||||
|
||||
<!-- Name + Tags -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="gi-name" data-i18n="game_integration.name">Name:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="game_integration.name.hint">A descriptive name for this game integration</small>
|
||||
<input type="text" id="gi-name" required>
|
||||
<div id="gi-tags-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="gi-description" data-i18n="game_integration.description">Description:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="game_integration.description.hint">Optional description of what this integration does</small>
|
||||
<input type="text" id="gi-description">
|
||||
</div>
|
||||
|
||||
<!-- Enabled -->
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="gi-enabled" checked>
|
||||
<span data-i18n="game_integration.enabled">Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Game / Adapter picker -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="gi-adapter-type" data-i18n="game_integration.adapter_type">Game / Adapter:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="game_integration.adapter_type.hint">Select the game or adapter type for this integration</small>
|
||||
<select id="gi-adapter-type"></select>
|
||||
</div>
|
||||
|
||||
<!-- Adapter config (auto-generated) -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="game_integration.adapter_config">Adapter Configuration</label>
|
||||
</div>
|
||||
<div id="gi-adapter-config-fields"></div>
|
||||
</div>
|
||||
|
||||
<!-- Setup instructions + Auto Setup buttons -->
|
||||
<div id="gi-setup-instructions-btn-wrapper" style="display:none">
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="openSetupInstructions()" data-i18n="game_integration.setup_instructions">Setup Instructions</button>
|
||||
<button type="button" id="gi-auto-setup-btn" class="btn btn-primary btn-sm" onclick="autoSetupGameIntegration()" style="display:none" data-i18n="game_integration.auto_setup">Auto Setup</button>
|
||||
</div>
|
||||
|
||||
<!-- Event Mapping Editor -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="game_integration.event_mappings">Event Mappings</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="game_integration.event_mappings.hint">Map game events to LED effects. Each event type can trigger a different visual effect.</small>
|
||||
|
||||
<div class="gi-mapping-toolbar">
|
||||
<select id="gi-mapping-preset" onchange="onMappingPresetChange()">
|
||||
<option value="" data-i18n="game_integration.preset.select">Load preset...</option>
|
||||
<option value="fps_combat" data-i18n="game_integration.preset.fps_combat">FPS Combat</option>
|
||||
<option value="moba_health" data-i18n="game_integration.preset.moba_health">MOBA Health</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary btn-sm" onclick="addGameMapping()" data-i18n="game_integration.mapping.add">+ Add Mapping</button>
|
||||
</div>
|
||||
<div id="gi-mappings-list" class="gi-mappings-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Live Event Monitor -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="game_integration.events.title">Live Events</label>
|
||||
</div>
|
||||
<div id="gi-event-feed" class="gi-event-feed"></div>
|
||||
</div>
|
||||
|
||||
<!-- Connection Test -->
|
||||
<div class="form-group">
|
||||
<button class="btn btn-secondary" onclick="testGameConnection()" data-i18n="game_integration.test.button">Test Connection</button>
|
||||
<div id="gi-test-panel" style="display:none" class="gi-test-panel"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-icon btn-secondary" onclick="closeGameIntegrationModal()" title="Cancel" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.cancel">✕</button>
|
||||
<button class="btn btn-icon btn-primary" onclick="saveGameIntegration()" title="Save" data-i18n-title="settings.button.save" data-i18n-aria-label="aria.save">✓</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Setup Instructions Overlay (full-screen, same pattern as release notes) -->
|
||||
<div id="gi-setup-overlay" class="log-overlay" style="display:none;">
|
||||
<button class="log-overlay-close" onclick="closeSetupInstructions()" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
||||
<div class="log-overlay-toolbar">
|
||||
<h3 id="gi-setup-overlay-title" data-i18n="game_integration.setup_instructions">Setup Instructions</h3>
|
||||
</div>
|
||||
<div id="gi-setup-overlay-content" class="release-notes-content"></div>
|
||||
</div>
|
||||
@@ -535,6 +535,78 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Event value source fields -->
|
||||
<div id="value-source-game-event-section" style="display:none">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="value-source-game-integration" data-i18n="value_source.game_event.integration">Game Integration:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="value_source.game_event.integration.hint">Select the game integration that provides events for this value source.</small>
|
||||
<select id="value-source-game-integration">
|
||||
<option value="">—</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="value-source-game-event-type" data-i18n="value_source.game_event.event_type">Event Type:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="value_source.game_event.event_type.hint">The continuous game event to track (health, mana, ammo, etc.).</small>
|
||||
<select id="value-source-game-event-type">
|
||||
<option value="health">health</option>
|
||||
<option value="armor">armor</option>
|
||||
<option value="mana">mana</option>
|
||||
<option value="ammo">ammo</option>
|
||||
<option value="stamina">stamina</option>
|
||||
<option value="shield">shield</option>
|
||||
<option value="score">score</option>
|
||||
<option value="gold">gold</option>
|
||||
<option value="xp">xp</option>
|
||||
<option value="level">level</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="value-source-ge-min"><span data-i18n="value_source.game_event.min_game_value">Min Game Value:</span> <span id="value-source-ge-min-display">0</span></label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="value_source.game_event.min_game_value.hint">Raw game value that maps to output 0.0.</small>
|
||||
<input type="number" id="value-source-ge-min" step="any" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="value-source-ge-max"><span data-i18n="value_source.game_event.max_game_value">Max Game Value:</span> <span id="value-source-ge-max-display">100</span></label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="value_source.game_event.max_game_value.hint">Raw game value that maps to output 1.0.</small>
|
||||
<input type="number" id="value-source-ge-max" step="any" value="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="value-source-ge-smoothing"><span data-i18n="value_source.game_event.smoothing">Smoothing:</span> <span id="value-source-ge-smoothing-display">0</span></label>
|
||||
<input type="range" id="value-source-ge-smoothing" min="0" max="0.99" step="0.01" value="0"
|
||||
oninput="document.getElementById('value-source-ge-smoothing-display').textContent = this.value">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="value-source-ge-default"><span data-i18n="value_source.game_event.default_value">Default Value:</span> <span id="value-source-ge-default-display">0.5</span></label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="value_source.game_event.default_value.hint">Output value when no events received within timeout.</small>
|
||||
<input type="range" id="value-source-ge-default" min="0" max="1" step="0.01" value="0.5"
|
||||
oninput="document.getElementById('value-source-ge-default-display').textContent = this.value">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="value-source-ge-timeout"><span data-i18n="value_source.game_event.timeout">Timeout (s):</span> <span id="value-source-ge-timeout-display">5.0</span></label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="value_source.game_event.timeout.hint">Seconds of silence before reverting to the default value.</small>
|
||||
<input type="range" id="value-source-ge-timeout" min="1" max="60" step="0.5" value="5"
|
||||
oninput="document.getElementById('value-source-ge-timeout-display').textContent = parseFloat(this.value).toFixed(1)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared adaptive output range (shown for adaptive and daylight types) -->
|
||||
<div id="value-source-adaptive-range-section" style="display:none">
|
||||
<div class="form-group">
|
||||
|
||||
Reference in New Issue
Block a user