Skip to content

Commit

Permalink
Tournament game service
Browse files Browse the repository at this point in the history
Closes #675
  • Loading branch information
1-alex98 committed Nov 11, 2022
1 parent 6768256 commit a93560d
Show file tree
Hide file tree
Showing 28 changed files with 1,593 additions and 52 deletions.
766 changes: 766 additions & 0 deletions .editorconfig

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
from .rating_service.rating_service import RatingService
from .servercontext import ServerContext
from .stats.game_stats_service import GameStatsService
from .tournament_service import TournamentService

__author__ = "Askaholic, Chris Kitching, Dragonfire, Gael Honorez, Jeroen De Dauw, Crotalus, Michael Søndergaard, Michel Jung"
__contact__ = "[email protected]"
Expand All @@ -134,6 +135,7 @@
"MessageQueueService",
"OAuthService",
"PartyService",
"TournamentService",
"PlayerService",
"RatingService",
"RatingService",
Expand All @@ -145,6 +147,7 @@
"run_control_server",
)


logger = logging.getLogger("server")

if config.ENABLE_METRICS:
Expand Down Expand Up @@ -197,6 +200,7 @@ def __init__(
party_service=self.services["party_service"],
rating_service=self.services["rating_service"],
oauth_service=self.services["oauth_service"],
tournament_service=self.services["tournament_service"],
)

def write_broadcast(
Expand Down
12 changes: 8 additions & 4 deletions server/game_service.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
"""
Manages the lifecycle of active games
"""

import asyncio
from collections import Counter
from typing import Optional, Union, ValuesView

import aiocron
from sqlalchemy import select

from server.config import config

from . import metrics
from .core import Service
from .db import FAFDatabase
Expand Down Expand Up @@ -145,7 +144,7 @@ def create_game(
visibility=VisibilityState.PUBLIC,
host: Optional[Player] = None,
name: Optional[str] = None,
mapname: Optional[str] = None,
map_name: Optional[str] = None,
password: Optional[str] = None,
matchmaker_queue_id: Optional[int] = None,
**kwargs
Expand All @@ -159,7 +158,7 @@ def create_game(
"id_": game_id,
"host": host,
"name": name,
"map_": mapname,
"map_name": map_name,
"game_mode": game_mode,
"game_service": self,
"game_stats_service": self.game_stats_service,
Expand Down Expand Up @@ -262,3 +261,8 @@ async def publish_game_results(self, game_results: EndedGameInfo):
metrics.rated_games.labels(game_results.rating_type).inc()
# TODO: Remove when rating service starts listening to message queue
await self._rating_service.enqueue(result_dict)


class NotConnectedError(asyncio.TimeoutError):
def __init__(self, players: list[Player]):
self.players = players
3 changes: 1 addition & 2 deletions server/gameconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,8 @@ async def handle_game_option(self, key: str, value: Any):
raw = repr(value)
self.game.map_scenario_path = \
raw.replace("\\", "/").replace("//", "/").replace("'", "")
self.game.map_file_path = "maps/{}.zip".format(
self.game.map_name = \
self.game.map_scenario_path.split("/")[2].lower()
)
elif key == "Title":
with contextlib.suppress(ValueError):
self.game.name = value
Expand Down
2 changes: 2 additions & 0 deletions server/games/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .custom_game import CustomGame
from .game import Game, GameError
from .ladder_game import LadderGame
from .tournament_game import TournamentGame
from .typedefs import (
FeaturedModType,
GameConnectionState,
Expand Down Expand Up @@ -41,6 +42,7 @@ class FeaturedMod(NamedTuple):
"GameType",
"InitMode",
"LadderGame",
"TournamentGame",
"ValidityState",
"Victory",
"VisibilityState",
Expand Down
48 changes: 40 additions & 8 deletions server/games/game.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import json
import logging
import re
import time
from collections import defaultdict
from typing import Any, Iterable, Optional
Expand Down Expand Up @@ -47,6 +48,9 @@ class GameError(Exception):
pass


MAP_FILE_PATH_PATTERN = re.compile(r"maps/(.+)\.zip")


class Game:
"""
Object that lasts for the lifetime of a game on FAF.
Expand All @@ -62,7 +66,7 @@ def __init__(
game_stats_service: "GameStatsService",
host: Optional[Player] = None,
name: str = "None",
map_: str = "SCMP_007",
map_name: str = "SCMP_007",
game_mode: str = FeaturedModType.FAF,
matchmaker_queue_id: Optional[int] = None,
rating_type: Optional[str] = None,
Expand All @@ -89,7 +93,7 @@ def __init__(
self.host = host
self.name = name
self.map_id = None
self.map_file_path = f"maps/{map_}.zip"
self.map_name = map_name
self.map_scenario_path = None
self.password = None
self._players_at_launch: list[Player] = []
Expand Down Expand Up @@ -153,6 +157,30 @@ def set_name_unchecked(self, value: str):
max_len = game_stats.c.gameName.type.length
self._name = value[:max_len]

@property
def map_name(self):
return self._map_name

@map_name.setter
def map_name(self, name: str):
self._map_name = name
self._map_file_path = f"maps/{name}.zip"

@property
def map_file_path(self):
return self._map_file_path

@map_file_path.setter
def map_file_path(self, path: str):
m = re.match(MAP_FILE_PATH_PATTERN, path)
if m is None:
raise ValueError(
"Map path must start with 'maps/' and end with '.zip'"
)

self._map_name = m.group(1)
self._map_file_path = path

@property
def armies(self) -> frozenset[int]:
return frozenset(
Expand Down Expand Up @@ -253,7 +281,7 @@ def get_team_sets(self) -> list[set[Player]]:
raise GameError(
"Missing team for at least one player. (player, team): {}"
.format([(player, self.get_player_option(player.id, "Team"))
for player in self.players])
for player in self.players])
)

teams = defaultdict(set)
Expand Down Expand Up @@ -439,7 +467,7 @@ async def on_game_finish(self):
await self.process_game_results()

self._process_pending_army_stats()
except Exception: # pragma: no cover
except Exception: # pragma: no cover
self._logger.exception("Error during game end")
finally:
self.state = GameState.ENDED
Expand Down Expand Up @@ -565,6 +593,7 @@ async def persist_results(self):
def get_basic_info(self) -> BasicGameInfo:
return BasicGameInfo(
self.id,
self.game_type,
self.rating_type,
self.map_id,
self.game_mode,
Expand Down Expand Up @@ -936,10 +965,7 @@ def map_folder_name(self) -> str:
try:
return str(self.map_scenario_path.split("/")[2]).lower()
except (IndexError, AttributeError):
if self.map_file_path:
return self.map_file_path[5:-4].lower()
else:
return "scmp_009"
return self.map_name

def __eq__(self, other):
if not isinstance(other, Game):
Expand All @@ -955,3 +981,9 @@ def __str__(self) -> str:
f"Game({self.id}, {self.host.login if self.host else ''}, "
f"{self.map_file_path})"
)

def wait_launched(self, param):
pass

def wait_hosted(self, param):
pass
12 changes: 12 additions & 0 deletions server/games/tournament_game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import logging

from . import LadderGame
from .typedefs import GameType

logger = logging.getLogger(__name__)


class TournamentGame(LadderGame):
"""Class for tournament games"""

game_type = GameType.TOURNAMENT
3 changes: 3 additions & 0 deletions server/games/typedefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class GameType(Enum):
COOP = "coop"
CUSTOM = "custom"
MATCHMAKER = "matchmaker"
TOURNAMENT = "tournament"


@unique
Expand Down Expand Up @@ -90,12 +91,14 @@ class BasicGameInfo(NamedTuple):
Holds basic information about a game that does not change after launch.
Fields:
- game_id: id of the game
- game_type: type of the game
- rating_type: str (e.g. "ladder1v1")
- map_id: id of the map used
- game_mode: name of the featured mod
"""

game_id: int
game_type: GameType
rating_type: Optional[str]
map_id: int
game_mode: str
Expand Down
19 changes: 7 additions & 12 deletions server/ladder_service/ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
matchmaker_queue_map_pool
)
from server.decorators import with_logger
from server.game_service import GameService
from server.games import InitMode, LadderGame
from server.game_service import GameService, NotConnectedError
from server.games import Game, InitMode, LadderGame
from server.games.ladder_game import GameClosedError
from server.ladder_service.game_name import game_name
from server.ladder_service.violation_service import ViolationService
Expand Down Expand Up @@ -563,23 +563,23 @@ def get_player_mean(player: Player) -> float:
if game_options:
game.gameOptions.update(game_options)

mapname = re.match("maps/(.+).zip", map_path).group(1)
map_name = re.match("maps/(.+).zip", map_path).group(1)
# FIXME: Database filenames contain the maps/ prefix and .zip suffix.
# Really in the future, just send a better description

self._logger.debug("Starting ladder game: %s", game)

def make_game_options(player: Player) -> GameLaunchOptions:
return GameLaunchOptions(
mapname=mapname,
mapname=map_name,
expected_players=len(all_players),
game_options=game_options,
team=game.get_player_option(player.id, "Team"),
faction=game.get_player_option(player.id, "Faction"),
map_position=game.get_player_option(player.id, "StartSpot")
)

await self.launch_match(game, host, all_guests, make_game_options)
await self.launch_server_made_game(game, host, all_guests, make_game_options)
self._logger.debug("Ladder game launched successfully %s", game)
metrics.matches.labels(queue.name, MatchLaunch.SUCCESSFUL).inc()
except Exception as e:
Expand Down Expand Up @@ -623,9 +623,9 @@ def make_game_options(player: Player) -> GameLaunchOptions:
)
self.violation_service.register_violations(abandoning_players)

async def launch_match(
async def launch_server_made_game(
self,
game: LadderGame,
game: Game,
host: Player,
guests: list[Player],
make_game_options: Callable[[Player], GameLaunchOptions]
Expand Down Expand Up @@ -725,8 +725,3 @@ def on_connection_lost(self, conn: "LobbyConnection") -> None:
async def shutdown(self):
for queue in self.queues.values():
queue.shutdown()


class NotConnectedError(asyncio.TimeoutError):
def __init__(self, players: list[Player]):
self.players = players
12 changes: 10 additions & 2 deletions server/lobbyconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from .protocol import DisconnectedError, Protocol
from .rating import InclusiveRange, RatingType
from .rating_service import RatingService
from .tournament_service import TournamentService
from .types import Address, GameLaunchOptions


Expand All @@ -75,6 +76,7 @@ def __init__(
party_service: PartyService,
rating_service: RatingService,
oauth_service: OAuthService,
tournament_service: TournamentService,
):
self._db = database
self.geoip_service = geoip
Expand All @@ -86,6 +88,7 @@ def __init__(
self.party_service = party_service
self.rating_service = rating_service
self.oauth_service = oauth_service
self.tournament_service = tournament_service
self._authenticated = False
self.player: Optional[Player] = None
self.game_connection: Optional[GameConnection] = None
Expand Down Expand Up @@ -946,7 +949,7 @@ async def command_game_host(self, message):
raise ClientError("Title must contain only ascii characters.")

mod = message.get("mod") or FeaturedModType.FAF
mapname = message.get("mapname") or "scmp_007"
map_name = message.get("mapname") or "scmp_007"
password = message.get("password")
game_mode = mod.lower()
rating_min = message.get("rating_min")
Expand All @@ -965,14 +968,19 @@ async def command_game_host(self, message):
game_class=game_class,
host=self.player,
name=title,
mapname=mapname,
map_name=map_name,
password=password,
rating_type=RatingType.GLOBAL,
displayed_rating_range=InclusiveRange(rating_min, rating_max),
enforce_rating_range=enforce_rating_range
)
await self.launch_game(game, is_host=True)

@player_idle("ready up for a tournament game")
async def command_is_ready_response(self, message):
assert isinstance(self.player, Player)
await self.tournament_service.on_is_ready_response(message, self.player)

async def command_match_ready(self, message):
"""
Replace with full implementation when implemented in client, see:
Expand Down
Loading

0 comments on commit a93560d

Please sign in to comment.