Skip to content

Commit 2d4b81e

Browse files
committed
Add logic to handle EstablishedPeer messages
1 parent 0dceb02 commit 2d4b81e

6 files changed

Lines changed: 390 additions & 7 deletions

File tree

server/game_connection_matrix.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from collections import defaultdict
2+
3+
4+
class ConnectionMatrix:
5+
def __init__(self, established_peers: dict[int, set[int]]):
6+
self.established_peers = established_peers
7+
8+
def get_unconnected_peer_ids(self) -> set[int]:
9+
unconnected_peer_ids: set[int] = set()
10+
11+
# Group players by number of connected peers
12+
players_by_num_peers = defaultdict(list)
13+
for player_id, peer_ids in self.established_peers.items():
14+
players_by_num_peers[len(peer_ids)].append((player_id, peer_ids))
15+
16+
# Mark players with least number of connections as unconnected if they
17+
# don't meet the connection threshold. Each time a player is marked as
18+
# 'unconnected', remaining players need 1 less connection to be
19+
# considered connected.
20+
connected_peers = dict(self.established_peers)
21+
for num_connected, peers in sorted(players_by_num_peers.items()):
22+
if num_connected < len(connected_peers) - 1:
23+
for player_id, peer_ids in peers:
24+
unconnected_peer_ids.add(player_id)
25+
del connected_peers[player_id]
26+
27+
return unconnected_peer_ids

server/gameconnection.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import contextlib
77
import json
88
import logging
9-
from typing import Any
9+
from typing import Any, Optional
1010

1111
from sqlalchemy import select
1212

@@ -62,6 +62,10 @@ def __init__(
6262
self.player = player
6363
player.game_connection = self # Set up weak reference to self
6464
self.game = game
65+
# None if the EstablishedPeers message is not implemented by the game
66+
# version/mode used by the player. For instance, matchmaker might have
67+
# it, but custom games might not.
68+
self.established_peer_ids: Optional[set[int]] = None
6569

6670
self.setup_timeout = setup_timeout
6771

@@ -561,15 +565,21 @@ async def handle_established_peer(self, peer_id: str):
561565
- `peer_id`: The identifier of the peer that this connection received
562566
the message from
563567
"""
564-
pass
568+
if self.established_peer_ids is None:
569+
self.established_peer_ids = set()
570+
571+
self.established_peer_ids.add(int(peer_id))
565572

566573
async def handle_disconnected_peer(self, peer_id: str):
567574
"""
568575
Sent by the lobby when a player disconnects from a peer. This can happen
569576
when a peer is rejoining in which case that peer will have reported a
570577
"Rejoining" status, or if the peer has exited the game.
571578
"""
572-
pass
579+
if self.established_peer_ids is None:
580+
self.established_peer_ids = set()
581+
582+
self.established_peer_ids.discard(int(peer_id))
573583

574584
def _mark_dirty(self):
575585
if self.game:

server/games/game.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
game_stats,
1818
matchmaker_queue_game
1919
)
20+
from server.game_connection_matrix import ConnectionMatrix
2021
from server.games.game_results import (
2122
ArmyOutcome,
2223
ArmyReportedOutcome,
@@ -211,13 +212,41 @@ def players(self) -> list[Player]:
211212

212213
def get_connected_players(self) -> list[Player]:
213214
"""
214-
Get a collection of all players currently connected to the game.
215+
Get a collection of all players currently connected to the host.
215216
"""
216217
return [
217218
player for player in self._connections.keys()
218219
if player.id in self._configured_player_ids
219220
]
220221

222+
def get_unconnected_players_from_peer_matrix(
223+
self,
224+
) -> Optional[list[Player]]:
225+
"""
226+
Get a list of players who are not fully connected to the game based on
227+
the established peers matrix if possible. The EstablishedPeers messages
228+
might not be implemented by the game in which case this returns None.
229+
"""
230+
if any(
231+
conn.established_peer_ids is None
232+
for conn in self._connections.values()
233+
):
234+
return None
235+
236+
matrix = ConnectionMatrix(
237+
established_peers={
238+
player.id: conn.established_peer_ids
239+
for player, conn in self._connections.items()
240+
}
241+
)
242+
unconnected_peer_ids = matrix.get_unconnected_peer_ids()
243+
244+
return [
245+
player
246+
for player in self._connections.keys()
247+
if player.id in unconnected_peer_ids
248+
]
249+
221250
def _is_observer(self, player: Player) -> bool:
222251
army = self.get_player_option(player.id, "Army")
223252
return army is None or army < 0

server/ladder_service/ladder_service.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,12 @@ async def launch_match(
667667
try:
668668
await game.wait_launched(60 + 10 * len(guests))
669669
except asyncio.TimeoutError:
670+
unconnected_players = game.get_unconnected_players_from_peer_matrix()
671+
if unconnected_players is not None:
672+
raise NotConnectedError(unconnected_players)
673+
674+
# If the connection matrix was not available, fall back to looking
675+
# at who was connected to the host only.
670676
connected_players = game.get_connected_players()
671677
raise NotConnectedError([
672678
player for player in guests

tests/integration_tests/test_matchmaker_violations.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44
from tests.utils import fast_forward
55

66
from .conftest import connect_and_sign_in, read_until_command
7-
from .test_game import open_fa, queue_players_for_matchmaking, start_search
7+
from .test_game import (
8+
client_response,
9+
open_fa,
10+
queue_players_for_matchmaking,
11+
send_player_options,
12+
start_search
13+
)
814
from .test_parties import accept_party_invite, invite_to_party
15+
from .test_teammatchmaker import \
16+
queue_players_for_matchmaking as queue_players_for_matchmaking_2v2
917

1018

1119
@fast_forward(360)
@@ -18,8 +26,7 @@ async def test_violation_for_guest_timeout(mocker, lobby_server):
1826

1927
# The player that queued last will be the host
2028
async def launch_game_and_timeout_guest():
21-
await read_until_command(host, "game_launch")
22-
await open_fa(host)
29+
await client_response(host, timeout=60)
2330
await read_until_command(host, "game_info")
2431

2532
await read_until_command(guest, "game_launch")
@@ -110,6 +117,64 @@ async def launch_game_and_timeout_guest():
110117
}
111118

112119

120+
@fast_forward(360)
121+
async def test_violation_for_guest_connected_to_host(mocker, lobby_server):
122+
mock_now = mocker.patch(
123+
"server.ladder_service.violation_service.datetime_now",
124+
return_value=datetime(2022, 2, 5, tzinfo=timezone.utc)
125+
)
126+
protos, ids = await queue_players_for_matchmaking_2v2(lobby_server)
127+
host, guest1, guest2, guest3 = protos
128+
host_id, guest1_id, guest2_id, guest3_id = ids
129+
130+
# Connect all players to the host
131+
await asyncio.gather(*[
132+
client_response(proto, timeout=60)
133+
for proto in protos
134+
])
135+
await send_player_options(
136+
host,
137+
[host_id, "Color", 1],
138+
[guest1_id, "Color", 2],
139+
[guest2_id, "Color", 3],
140+
[guest3_id, "Color", 4],
141+
)
142+
143+
# Set up connection matrix
144+
# Guest3 only connects to the host
145+
for id in (guest1_id, guest2_id, guest3_id):
146+
await host.send_message({
147+
"target": "game",
148+
"command": "EstablishedPeer",
149+
"args": [id],
150+
})
151+
for id in (host_id, guest2_id):
152+
await guest1.send_message({
153+
"target": "game",
154+
"command": "EstablishedPeer",
155+
"args": [id],
156+
})
157+
for id in (host_id, guest1_id):
158+
await guest2.send_message({
159+
"target": "game",
160+
"command": "EstablishedPeer",
161+
"args": [id],
162+
})
163+
await guest3.send_message({
164+
"target": "game",
165+
"command": "EstablishedPeer",
166+
"args": [guest3_id],
167+
})
168+
169+
await read_until_command(host, "match_cancelled", timeout=120)
170+
msg = await read_until_command(guest3, "search_violation", timeout=10)
171+
assert msg == {
172+
"command": "search_violation",
173+
"count": 1,
174+
"time": "2022-02-05T00:00:00+00:00",
175+
}
176+
177+
113178
@fast_forward(360)
114179
async def test_violation_persisted_across_logins(mocker, lobby_server):
115180
mocker.patch(

0 commit comments

Comments
 (0)