Files
ledgrab/server/tests/core/test_dota2_adapter.py
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

205 lines
7.3 KiB
Python

"""Tests for Dota 2 Game State Integration adapter."""
import pytest
from wled_controller.core.game_integration.adapters.dota2_adapter import Dota2Adapter
def _make_dota2_payload(
*,
health: int = 1000,
max_health: int = 1000,
mana: float = 500.0,
max_mana: float = 500.0,
gold: int = 625,
kills: int = 0,
deaths: int = 0,
game_state: str | None = None,
auth_token: str | None = None,
) -> dict:
"""Build a realistic Dota 2 GSI payload."""
payload: dict = {
"hero": {
"xpos": -6624,
"ypos": -6592,
"id": 1,
"name": "npc_dota_hero_antimage",
"level": 12,
"alive": True,
"respawn_seconds": 0,
"buyback_cost": 874,
"buyback_cooldown": 0,
"health": health,
"max_health": max_health,
"health_percent": int(100.0 * health / max_health) if max_health else 0,
"mana": mana,
"max_mana": max_mana,
"mana_percent": int(100.0 * mana / max_mana) if max_mana else 0,
},
"player": {
"steamid": "76561198012345678",
"name": "TestPlayer",
"activity": "playing",
"kills": kills,
"deaths": deaths,
"assists": 0,
"last_hits": 120,
"denies": 10,
"kill_streak": 0,
"gold": gold,
"gold_reliable": 200,
"gold_unreliable": gold - 200,
"gpm": 450,
"xpm": 520,
},
}
if game_state is not None:
payload["map"] = {
"name": "start",
"matchid": "12345",
"game_time": 1200,
"clock_time": 1200,
"daytime": True,
"game_state": game_state,
"win_team": "none",
}
if auth_token is not None:
payload["auth"] = {"token": auth_token}
return payload
class TestDota2ContinuousEvents:
def test_health_normalized(self) -> None:
payload = _make_dota2_payload(health=750, max_health=1500)
events, _ = Dota2Adapter.parse_payload(payload, {"adapter_id": "d2"}, {})
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_dota2_payload(mana=300.0, max_mana=600.0)
events, _ = Dota2Adapter.parse_payload(payload, {"adapter_id": "d2"}, {})
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_gold_normalized(self) -> None:
payload = _make_dota2_payload(gold=5000)
events, _ = Dota2Adapter.parse_payload(payload, {"adapter_id": "d2"}, {})
g = [e for e in events if e.event_type == "gold"]
assert len(g) == 1
assert g[0].value == pytest.approx(5000.0 / 99999.0)
def test_gold_custom_max(self) -> None:
payload = _make_dota2_payload(gold=5000)
events, _ = Dota2Adapter.parse_payload(
payload,
{"adapter_id": "d2", "max_gold": 10000},
{},
)
g = [e for e in events if e.event_type == "gold"]
assert g[0].value == pytest.approx(0.5)
class TestDota2DiffDetection:
def test_kill_detected(self) -> None:
p1 = _make_dota2_payload(kills=3)
_, s1 = Dota2Adapter.parse_payload(p1, {"adapter_id": "d2"}, {})
p2 = _make_dota2_payload(kills=4)
events, _ = Dota2Adapter.parse_payload(p2, {"adapter_id": "d2"}, s1)
kills = [e for e in events if e.event_type == "kill"]
assert len(kills) == 1
def test_no_kill_on_first_payload(self) -> None:
p = _make_dota2_payload(kills=5)
events, _ = Dota2Adapter.parse_payload(p, {"adapter_id": "d2"}, {})
kills = [e for e in events if e.event_type == "kill"]
assert len(kills) == 0
def test_death_detected(self) -> None:
p1 = _make_dota2_payload(deaths=1)
_, s1 = Dota2Adapter.parse_payload(p1, {"adapter_id": "d2"}, {})
p2 = _make_dota2_payload(deaths=2)
events, _ = Dota2Adapter.parse_payload(p2, {"adapter_id": "d2"}, s1)
deaths = [e for e in events if e.event_type == "death"]
assert len(deaths) == 1
def test_multiple_kills(self) -> None:
p1 = _make_dota2_payload(kills=0)
_, s1 = Dota2Adapter.parse_payload(p1, {"adapter_id": "d2"}, {})
p2 = _make_dota2_payload(kills=3)
events, _ = Dota2Adapter.parse_payload(p2, {"adapter_id": "d2"}, s1)
kills = [e for e in events if e.event_type == "kill"]
assert len(kills) == 3
class TestDota2MatchFlow:
def test_match_start_pre_game(self) -> None:
p = _make_dota2_payload(game_state="DOTA_GAMERULES_STATE_PRE_GAME")
events, _ = Dota2Adapter.parse_payload(p, {"adapter_id": "d2"}, {})
ms = [e for e in events if e.event_type == "match_start"]
assert len(ms) == 1
def test_match_start_in_progress(self) -> None:
prev = {"game_state": "DOTA_GAMERULES_STATE_PRE_GAME"}
p = _make_dota2_payload(game_state="DOTA_GAMERULES_STATE_GAME_IN_PROGRESS")
events, _ = Dota2Adapter.parse_payload(p, {"adapter_id": "d2"}, prev)
ms = [e for e in events if e.event_type == "match_start"]
assert len(ms) == 1
def test_match_end(self) -> None:
prev = {"game_state": "DOTA_GAMERULES_STATE_GAME_IN_PROGRESS"}
p = _make_dota2_payload(game_state="DOTA_GAMERULES_STATE_POST_GAME")
events, _ = Dota2Adapter.parse_payload(p, {"adapter_id": "d2"}, prev)
me = [e for e in events if e.event_type == "match_end"]
assert len(me) == 1
def test_no_event_on_same_state(self) -> None:
prev = {"game_state": "DOTA_GAMERULES_STATE_GAME_IN_PROGRESS"}
p = _make_dota2_payload(game_state="DOTA_GAMERULES_STATE_GAME_IN_PROGRESS")
events, _ = Dota2Adapter.parse_payload(p, {"adapter_id": "d2"}, prev)
flow = [e for e in events if e.event_type in ("match_start", "match_end")]
assert len(flow) == 0
class TestDota2Auth:
def test_auth_valid(self) -> None:
p = _make_dota2_payload(auth_token="secret")
result = Dota2Adapter.validate_auth({}, p, {"auth_token": "secret"})
assert result is True
def test_auth_invalid(self) -> None:
p = _make_dota2_payload(auth_token="wrong")
result = Dota2Adapter.validate_auth({}, p, {"auth_token": "secret"})
assert result is False
def test_auth_no_config_accepts_all(self) -> None:
p = _make_dota2_payload()
result = Dota2Adapter.validate_auth({}, p, {})
assert result is True
class TestDota2Metadata:
def test_adapter_type(self) -> None:
assert Dota2Adapter.ADAPTER_TYPE == "dota2"
def test_supported_events(self) -> None:
assert "health" in Dota2Adapter.SUPPORTED_EVENTS
assert "mana" in Dota2Adapter.SUPPORTED_EVENTS
assert "gold" in Dota2Adapter.SUPPORTED_EVENTS
def test_config_schema(self) -> None:
schema = Dota2Adapter.get_config_schema()
assert "auth_token" in schema["properties"]
assert "max_gold" in schema["properties"]
def test_setup_instructions(self) -> None:
instructions = Dota2Adapter.get_setup_instructions()
assert "Dota 2" in instructions