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
205 lines
7.3 KiB
Python
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
|