Skip to content

Commit

Permalink
Choose player factions after a match is found
Browse files Browse the repository at this point in the history
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 FAForever#679
  • Loading branch information
Askaholic committed Oct 7, 2020
1 parent 2946c4a commit 80feb6c
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 23 deletions.
12 changes: 11 additions & 1 deletion server/factions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from enum import IntEnum, unique
from typing import Union


@unique
Expand All @@ -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)}!")
15 changes: 12 additions & 3 deletions server/ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions server/lobbyconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion server/matchmaker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
14 changes: 9 additions & 5 deletions server/matchmaker/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
8 changes: 2 additions & 6 deletions server/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions server/team_matchmaker/player_party.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion tests/integration_tests/test_teammatchmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down

0 comments on commit 80feb6c

Please sign in to comment.