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
184 lines
6.5 KiB
Python
184 lines
6.5 KiB
Python
"""Tests for League of Legends Live Client Data API adapter."""
|
|
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
from wled_controller.core.game_integration.adapters.lol_adapter import (
|
|
LoLAdapter,
|
|
LoLPoller,
|
|
)
|
|
|
|
|
|
def _make_lol_payload(
|
|
*,
|
|
current_health: float = 1000.0,
|
|
max_health: float = 1000.0,
|
|
resource_value: float = 500.0,
|
|
resource_max: float = 500.0,
|
|
level: int = 10,
|
|
summoner_name: str = "TestPlayer",
|
|
current_gold: float | None = None,
|
|
) -> dict:
|
|
"""Build a realistic LoL Live Client Data payload."""
|
|
payload: dict = {
|
|
"activePlayer": {
|
|
"summonerName": summoner_name,
|
|
"level": level,
|
|
"currentHealth": current_health,
|
|
"championStats": {
|
|
"currentHealth": current_health,
|
|
"maxHealth": max_health,
|
|
"resourceValue": resource_value,
|
|
"resourceMax": resource_max,
|
|
"attackDamage": 80.0,
|
|
"abilityPower": 0.0,
|
|
"armor": 60.0,
|
|
"magicResist": 40.0,
|
|
"moveSpeed": 345.0,
|
|
},
|
|
},
|
|
"allPlayers": [
|
|
{
|
|
"summonerName": summoner_name,
|
|
"championName": "Jinx",
|
|
"team": "ORDER",
|
|
"scores": {"kills": 5, "deaths": 2, "assists": 7},
|
|
},
|
|
],
|
|
"gameData": {
|
|
"gameMode": "CLASSIC",
|
|
"gameTime": 1200.5,
|
|
"mapName": "Map11",
|
|
"mapNumber": 11,
|
|
"mapTerrain": "Default",
|
|
},
|
|
}
|
|
|
|
if current_gold is not None:
|
|
payload["allPlayers"][0]["currentGold"] = current_gold
|
|
|
|
return payload
|
|
|
|
|
|
class TestLoLContinuousEvents:
|
|
def test_health_normalized(self) -> None:
|
|
payload = _make_lol_payload(current_health=500.0, max_health=1000.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
hp = [e for e in events if e.event_type == "health"]
|
|
assert len(hp) == 1
|
|
assert hp[0].value == pytest.approx(0.5)
|
|
|
|
def test_mana_normalized(self) -> None:
|
|
payload = _make_lol_payload(resource_value=300.0, resource_max=600.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
mp = [e for e in events if e.event_type == "mana"]
|
|
assert len(mp) == 1
|
|
assert mp[0].value == pytest.approx(0.5)
|
|
|
|
def test_level_normalized(self) -> None:
|
|
payload = _make_lol_payload(level=9)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
lvl = [e for e in events if e.event_type == "speed"]
|
|
assert len(lvl) == 1
|
|
assert lvl[0].value == pytest.approx(9.0 / 18.0)
|
|
|
|
def test_level_max(self) -> None:
|
|
payload = _make_lol_payload(level=18)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
lvl = [e for e in events if e.event_type == "speed"]
|
|
assert lvl[0].value == pytest.approx(1.0)
|
|
|
|
def test_gold(self) -> None:
|
|
payload = _make_lol_payload(current_gold=15000.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
|
|
gold = [e for e in events if e.event_type == "gold"]
|
|
assert len(gold) == 1
|
|
assert gold[0].value == pytest.approx(15000.0 / 30000.0)
|
|
|
|
|
|
class TestLoLDeathRespawn:
|
|
def test_death_detected_when_health_drops_to_zero(self) -> None:
|
|
prev = {"alive": True}
|
|
payload = _make_lol_payload(current_health=0.0, max_health=1000.0)
|
|
events, state = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
|
|
deaths = [e for e in events if e.event_type == "death"]
|
|
assert len(deaths) == 1
|
|
assert state["alive"] is False
|
|
|
|
def test_no_death_when_already_dead(self) -> None:
|
|
prev = {"alive": False}
|
|
payload = _make_lol_payload(current_health=0.0, max_health=1000.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
|
|
deaths = [e for e in events if e.event_type == "death"]
|
|
assert len(deaths) == 0
|
|
|
|
def test_respawn_detected(self) -> None:
|
|
prev = {"alive": False}
|
|
payload = _make_lol_payload(current_health=800.0, max_health=1000.0)
|
|
events, state = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
|
|
respawns = [e for e in events if e.event_type == "objective_progress"]
|
|
assert len(respawns) == 1
|
|
assert state["alive"] is True
|
|
|
|
def test_no_respawn_when_already_alive(self) -> None:
|
|
prev = {"alive": True}
|
|
payload = _make_lol_payload(current_health=800.0, max_health=1000.0)
|
|
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
|
|
respawns = [e for e in events if e.event_type == "objective_progress"]
|
|
assert len(respawns) == 0
|
|
|
|
|
|
class TestLoLAuth:
|
|
def test_always_accepts(self) -> None:
|
|
assert LoLAdapter.validate_auth({}, {}, {}) is True
|
|
assert LoLAdapter.validate_auth({"X-Custom": "val"}, {}, {"auth_token": "x"}) is True
|
|
|
|
|
|
class TestLoLMetadata:
|
|
def test_adapter_type(self) -> None:
|
|
assert LoLAdapter.ADAPTER_TYPE == "lol"
|
|
|
|
def test_supported_events(self) -> None:
|
|
assert "health" in LoLAdapter.SUPPORTED_EVENTS
|
|
assert "mana" in LoLAdapter.SUPPORTED_EVENTS
|
|
assert "death" in LoLAdapter.SUPPORTED_EVENTS
|
|
|
|
def test_config_schema(self) -> None:
|
|
schema = LoLAdapter.get_config_schema()
|
|
assert "poll_interval_ms" in schema["properties"]
|
|
|
|
def test_setup_instructions(self) -> None:
|
|
instructions = LoLAdapter.get_setup_instructions()
|
|
assert "League of Legends" in instructions
|
|
|
|
|
|
class TestLoLPoller:
|
|
def test_start_stop(self) -> None:
|
|
"""Test that the poller starts and stops cleanly."""
|
|
called = threading.Event()
|
|
|
|
def callback(data: dict) -> None:
|
|
called.set()
|
|
|
|
poller = LoLPoller({"poll_interval_ms": 100}, callback)
|
|
assert not poller.is_running
|
|
|
|
poller.start()
|
|
assert poller.is_running
|
|
|
|
poller.stop()
|
|
assert not poller.is_running
|
|
|
|
def test_double_start_no_crash(self) -> None:
|
|
"""Starting twice should not create duplicate threads."""
|
|
poller = LoLPoller({"poll_interval_ms": 1000}, lambda d: None)
|
|
poller.start()
|
|
poller.start() # should warn but not crash
|
|
poller.stop()
|
|
|
|
def test_stop_without_start(self) -> None:
|
|
"""Stopping without starting should not crash."""
|
|
poller = LoLPoller({"poll_interval_ms": 1000}, lambda d: None)
|
|
poller.stop() # no-op
|