From 80feb6cfc7a57c2d98fd2b7d45845bfbd48aec53 Mon Sep 17 00:00:00 2001 From: Askaholic Date: Mon, 5 Oct 2020 23:07:07 -0800 Subject: [PATCH] Choose player factions after a match is found As searches can only be started by party owners, the guests may not have decided what factions they want to pick when the owner enters them into the matchmaker queue. Therefore we should allow them to change their faction preferences up until the moment they are matched. Fixes #679 --- server/factions.py | 12 +++++++++++- server/ladder_service.py | 15 ++++++++++++--- server/lobbyconnection.py | 13 +++++++------ server/matchmaker/__init__.py | 3 ++- server/matchmaker/search.py | 14 +++++++++----- server/players.py | 8 ++------ server/team_matchmaker/player_party.py | 5 +++++ tests/integration_tests/test_teammatchmaker.py | 7 ++++++- 8 files changed, 54 insertions(+), 23 deletions(-) diff --git a/server/factions.py b/server/factions.py index 6ce5cae3a..a2ceee22e 100644 --- a/server/factions.py +++ b/server/factions.py @@ -1,4 +1,5 @@ from enum import IntEnum, unique +from typing import Union @unique @@ -13,4 +14,13 @@ class Faction(IntEnum): @staticmethod def from_string(value: str) -> "Faction": - return Faction.__members__[value] + return Faction.__members__[value.lower()] + + @staticmethod + def from_value(value: Union[str, int]) -> "Faction": + if isinstance(value, str): + return Faction.from_string(value) + elif isinstance(value, int): + return Faction(value) + + raise TypeError(f"Unsupported faction type {type(value)}!") diff --git a/server/ladder_service.py b/server/ladder_service.py index 60c414fa4..cc52bad88 100644 --- a/server/ladder_service.py +++ b/server/ladder_service.py @@ -30,7 +30,7 @@ from .decorators import with_logger from .game_service import GameService from .games import LadderGame -from .matchmaker import MapPool, MatchmakerQueue, Search +from .matchmaker import MapPool, MatchmakerQueue, OnMatchedCallback, Search from .players import Player, PlayerState from .protocol import DisconnectedError from .rating import RatingType @@ -165,14 +165,23 @@ async def fetch_matchmaker_queues(self, conn): )) return matchmaker_queues - def start_search(self, players: List[Player], queue_name: str): + def start_search( + self, + players: List[Player], + queue_name: str, + on_matched: OnMatchedCallback = lambda _1, _2: None + ): # Cancel any existing searches that players have for this queue for player in players: if queue_name in self._searches[player]: self._cancel_search(player, queue_name) queue = self.queues[queue_name] - search = Search(players, rating_type=queue.rating_type) + search = Search( + players, + rating_type=queue.rating_type, + on_matched=on_matched + ) for player in players: player.state = PlayerState.SEARCHING_LADDER diff --git a/server/lobbyconnection.py b/server/lobbyconnection.py index 0a18adfc5..ee21e2ccb 100644 --- a/server/lobbyconnection.py +++ b/server/lobbyconnection.py @@ -855,16 +855,17 @@ async def command_game_matchmaking(self, message): recoverable=True ) - for member in party: - member.set_player_faction() - # TODO: Remove this legacy behavior, use party instead if "faction" in message: - self.player.faction = message["faction"] + party.set_factions( + self.player, + [Faction.from_value(message["faction"])] + ) self.ladder_service.start_search( players, - queue_name=queue_name + queue_name=queue_name, + on_matched=party.on_matched ) @ice_only @@ -1075,7 +1076,7 @@ async def command_leave_party(self, _message): await self.party_service.leave_party(self.player) async def command_set_party_factions(self, message): - factions = set(Faction.from_string(str(i).lower()) for i in message["factions"]) + factions = set(Faction.from_value(v) for v in message["factions"]) if not factions: raise ClientError( diff --git a/server/matchmaker/__init__.py b/server/matchmaker/__init__.py index 7ae8c9261..63068ebdf 100644 --- a/server/matchmaker/__init__.py +++ b/server/matchmaker/__init__.py @@ -7,12 +7,13 @@ from .map_pool import MapPool from .matchmaker_queue import MatchmakerQueue from .pop_timer import PopTimer -from .search import CombinedSearch, Search +from .search import CombinedSearch, OnMatchedCallback, Search __all__ = ( "CombinedSearch", "MapPool", "MatchmakerQueue", + "OnMatchedCallback", "PopTimer", "Search", ) diff --git a/server/matchmaker/search.py b/server/matchmaker/search.py index f6ff63f29..28e632d26 100644 --- a/server/matchmaker/search.py +++ b/server/matchmaker/search.py @@ -2,7 +2,7 @@ import itertools import math import time -from typing import List, Optional, Tuple +from typing import Any, Callable, List, Optional, Tuple from trueskill import Rating, quality @@ -12,6 +12,9 @@ from ..decorators import with_logger from ..players import Player +Match = Tuple["Search", "Search"] +OnMatchedCallback = Callable[["Search", "Search"], Any] + @with_logger class Search: @@ -23,7 +26,8 @@ def __init__( self, players: List[Player], start_time: Optional[float] = None, - rating_type: str = RatingType.LADDER_1V1 + rating_type: str = RatingType.LADDER_1V1, + on_matched: OnMatchedCallback = lambda _1, _2: None ): """ Default ctor for a search @@ -42,6 +46,7 @@ def __init__( self.start_time = start_time or time.time() self._match = asyncio.Future() self._failed_matching_attempts = 0 + self.on_matched = on_matched # Precompute this self.quality_against_self = self.quality_with(self) @@ -201,6 +206,8 @@ def match(self, other: "Search"): """ self._logger.info("Matched %s with %s", self.players, other.players) + self.on_matched(self, other) + for player, raw_rating in zip(self.players, self.raw_ratings): if self.is_ladder1v1_search() and self._is_ladder_newbie(player): mean, dev = raw_rating @@ -307,6 +314,3 @@ def cancel(self): def __str__(self): return "CombinedSearch({})".format(",".join(str(s) for s in self.searches)) - - -Match = Tuple[Search, Search] diff --git a/server/players.py b/server/players.py index aecf95572..fc1ff2617 100644 --- a/server/players.py +++ b/server/players.py @@ -81,14 +81,10 @@ def faction(self) -> Faction: @faction.setter def faction(self, value: Union[str, int, Faction]) -> None: - if isinstance(value, str): - self._faction = Faction.from_string(value) - elif isinstance(value, int): - self._faction = Faction(value) - elif isinstance(value, Faction): + if isinstance(value, Faction): self._faction = value else: - raise TypeError(f"Unsupported faction type {type(value)}!") + self._faction = Faction.from_value(value) def power(self) -> int: """An artifact of the old permission system. The client still uses this diff --git a/server/team_matchmaker/player_party.py b/server/team_matchmaker/player_party.py index cdf6f3860..cab314a3e 100644 --- a/server/team_matchmaker/player_party.py +++ b/server/team_matchmaker/player_party.py @@ -2,6 +2,7 @@ from typing import FrozenSet, List, NamedTuple, Optional from server.factions import Faction +from server.matchmaker import Search from server.players import Player from server.team_matchmaker.party_member import PartyMember @@ -68,6 +69,10 @@ def remove_invited_player(self, player: Player) -> None: def set_factions(self, player: Player, factions: List[Faction]) -> None: self._members[player].factions = factions + def on_matched(self, search1: Search, search2: Search) -> None: + for member in self: + member.set_player_faction() + async def send_party(self, player: Player) -> None: await player.send_message({ "command": "update_party", diff --git a/tests/integration_tests/test_teammatchmaker.py b/tests/integration_tests/test_teammatchmaker.py index 245f25127..330291086 100644 --- a/tests/integration_tests/test_teammatchmaker.py +++ b/tests/integration_tests/test_teammatchmaker.py @@ -141,7 +141,7 @@ async def test_game_matchmaking_with_parties(lobby_server): await proto1.send_message({ "command": "set_party_factions", - "factions": ["uef"] + "factions": ["seraphim"] }) await proto2.send_message({ "command": "set_party_factions", @@ -164,6 +164,11 @@ async def test_game_matchmaking_with_parties(lobby_server): "queue_name": "tmm2v2", "state": "start", }) + # Change faction selection after queueing + await proto1.send_message({ + "command": "set_party_factions", + "factions": ["uef"] + }) await proto3.send_message({ "command": "game_matchmaking", "queue_name": "tmm2v2",