Skip to content

Commit

Permalink
Issue/#602 search start confirmation (#604)
Browse files Browse the repository at this point in the history
* Rename `game_launch_cancelled` to `match_cancelled`

* Add `search_started` command when starting a ladder search

* Combine messages into one `search_info` command
  • Loading branch information
Askaholic authored Jun 10, 2020
1 parent 584adb4 commit ccf349e
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 32 deletions.
56 changes: 43 additions & 13 deletions server/ladder_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import contextlib
from collections import defaultdict
from typing import Dict, List, Set, Tuple
from typing import Dict, List, Optional, Set, Tuple

import aiocron
from sqlalchemy import and_, func, select, text
Expand Down Expand Up @@ -158,11 +158,16 @@ async def fetch_matchmaker_queues(self, conn) -> Dict[str, Tuple[int, int, int]]
))
return matchmaker_queues

async def start_search(self, initiator: Player, search: Search, queue_name: str):
async def start_search(
self,
initiator: Player,
search: Search,
queue_name: str
):
# TODO: Consider what happens if players disconnect while starting
# search. Will need a message to inform other players in the search
# that it has been cancelled.
self._cancel_existing_searches(initiator)
self._cancel_existing_searches(initiator, queue_name)

tasks = []
for player in search.players:
Expand All @@ -172,6 +177,12 @@ async def start_search(self, initiator: Player, search: Search, queue_name: str)
if queue_name == "ladder1v1":
tasks.append(self.inform_player(player))

tasks.append(player.send_message({
"command": "search_info",
"queue": queue_name,
"state": "start"
}))

try:
await asyncio.gather(*tasks)
except DisconnectedError:
Expand All @@ -189,33 +200,52 @@ async def start_search(self, initiator: Player, search: Search, queue_name: str)

asyncio.create_task(self.queues[queue_name].search(search))

async def cancel_search(self, initiator: Player):
searches = self._cancel_existing_searches(initiator)
async def cancel_search(
self,
initiator: Player,
queue_name: Optional[str] = None
):
searches = self._cancel_existing_searches(initiator, queue_name)

tasks = []
for search in searches:
for queue_name, search in searches:
for player in search.players:
# FIXME: This is wrong for multiqueueing
if player.state == PlayerState.SEARCHING_LADDER:
player.state = PlayerState.IDLE

if player.lobby_connection is not None:
tasks.append(player.send_message({
"command": "game_matchmaking",
"command": "search_info",
"queue": queue_name,
"state": "stop"
}))
self._logger.info(
"%s stopped searching for ladder: %s", player, search
"%s stopped searching for %s: %s", initiator, queue_name, search
)

await gather_without_exceptions(tasks, DisconnectedError)

def _cancel_existing_searches(self, initiator: Player) -> List[Search]:
def _cancel_existing_searches(
self,
initiator: Player,
queue_name: Optional[str] = None
) -> List[Tuple[str, Search]]:
"""
Cancel search for a specific queue, or all searches if `queue_name` is
None.
"""
if queue_name:
queue_names = [queue_name]
else:
queue_names = list(self.queues)

searches = []
for queue_name in self.queues:
for queue_name in queue_names:
search = self.searches[queue_name].get(initiator)
if search:
search.cancel()
searches.append(search)
searches.append((queue_name, search))
del self.searches[queue_name][initiator]
return searches

Expand Down Expand Up @@ -338,7 +368,7 @@ async def start_game(self, host: Player, guest: Player):
if not hosted:
raise TimeoutError("Host left lobby")
finally:
# TODO: Once the client supports `game_launch_cancelled`, don't
# TODO: Once the client supports `match_cancelled`, don't
# send `launch_game` to the client if the host timed out. Until
# then, failing to send `launch_game` will cause the client to
# think it is searching for ladder, even though the server has
Expand All @@ -356,7 +386,7 @@ async def start_game(self, host: Player, guest: Player):
self._logger.debug("Ladder game launched successfully")
except Exception:
self._logger.exception("Failed to start ladder game!")
msg = {"command": "game_launch_cancelled"}
msg = {"command": "match_cancelled"}
with contextlib.suppress(DisconnectedError):
await asyncio.gather(
host.send_message(msg),
Expand Down
2 changes: 1 addition & 1 deletion server/lobbyconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ async def command_game_matchmaking(self, message):
raise ClientError("Cannot host game. Please update your client to the newest version.")

if state == "stop":
await self.ladder_service.cancel_search(self.player)
await self.ladder_service.cancel_search(self.player, mod)
return

if state == "start":
Expand Down
55 changes: 48 additions & 7 deletions tests/integration_tests/test_matchmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ async def queue_player_for_matchmaking(user, lobby_server):
'state': 'start',
'faction': 'uef'
})
await read_until_command(proto, 'search_info')

return proto

Expand All @@ -39,6 +40,7 @@ async def queue_players_for_matchmaking(lobby_server):
'state': 'start',
'faction': 1 # Python client sends factions as numbers
})
await read_until_command(proto2, 'search_info')

# If the players did not match, this will fail due to a timeout error
await read_until_command(proto1, 'match_found')
Expand Down Expand Up @@ -145,11 +147,11 @@ async def test_game_matchmaking_timeout(lobby_server):
msg2 = await read_until_command(proto2, 'game_launch')
# LEGACY BEHAVIOUR: The host does not respond with the appropriate GameState
# so the match is cancelled. However, the client does not know how to
# handle `game_launch_cancelled` messages so we still send `game_launch` to
# handle `match_cancelled` messages so we still send `game_launch` to
# prevent the client from showing that it is searching when it really isn't.
msg1 = await read_until_command(proto1, 'game_launch')
await read_until_command(proto2, 'game_launch_cancelled')
await read_until_command(proto1, 'game_launch_cancelled')
await read_until_command(proto2, 'match_cancelled')
await read_until_command(proto1, 'match_cancelled')

assert msg1['uid'] == msg2['uid']
assert msg1['mod'] == 'ladder1v1'
Expand All @@ -168,10 +170,11 @@ async def test_game_matchmaking_cancel(lobby_server):
})

# The server should respond with a matchmaking stop message
msg = await read_until_command(proto, 'game_matchmaking')
msg = await read_until_command(proto, 'search_info')

assert msg == {
'command': 'game_matchmaking',
'command': 'search_info',
'queue': 'ladder1v1',
'state': 'stop',
}

Expand All @@ -182,9 +185,9 @@ async def test_game_matchmaking_disconnect(lobby_server):
# One player disconnects before the game has launched
await proto1.close()

msg = await read_until_command(proto2, 'game_launch_cancelled')
msg = await read_until_command(proto2, 'match_cancelled')

assert msg == {'command': 'game_launch_cancelled'}
assert msg == {'command': 'match_cancelled'}


@fast_forward(100)
Expand Down Expand Up @@ -282,3 +285,41 @@ async def read_update_msg():

assert msg["queues"][0]["queue_name"] == "ladder1v1"
assert len(msg["queues"][0]["boundary_80s"]) == 0


@fast_forward(10)
async def test_search_info_messages(lobby_server):
_, _, proto = await connect_and_sign_in(
("ladder1", "ladder1"),
lobby_server
)
await read_until_command(proto, "game_info")

# Start searching
await proto.send_message({
"command": "game_matchmaking",
"state": "start",
"faction": "uef"
})
msg = await read_until_command(proto, "search_info")
assert msg == {
"command": "search_info",
"queue": "ladder1v1",
"state": "start"
}
# TODO: Join a second queue here

# Stop searching
await proto.send_message({
"command": "game_matchmaking",
"state": "stop",
})
msg = await read_until_command(proto, "search_info")
assert msg == {
"command": "search_info",
"queue": "ladder1v1",
"state": "stop"
}

with pytest.raises(asyncio.TimeoutError):
await read_until_command(proto, "search_info", timeout=5)
Loading

0 comments on commit ccf349e

Please sign in to comment.