diff --git a/Dockerfile b/Dockerfile index ce44f206c..a53b07b9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ USER faf # Main entrypoint and the default command that will be run CMD ["/usr/local/bin/python3", "main.py"] -# lobby server runs on 8001/tcp (QDataStream) and 8002/tcp (JSON) -EXPOSE 8001 8002 +# lobby server runs on 8002/tcp (JSON) +EXPOSE 8002 RUN python3 -V diff --git a/README.md b/README.md index 3ab5f9a05..dafcfd18b 100644 --- a/README.md +++ b/README.md @@ -82,26 +82,8 @@ list see [https://faforever.github.io/server/](https://faforever.github.io/serve The protocol is mainly JSON-encoded maps, containing at minimum a `command` key, representing the command to dispatch. -The wire format uses [QDataStream](http://doc.qt.io/qt-5/qdatastream.html) (UTF-16, BigEndian). - -For the lobbyconnection, each message is of the form: -``` -ACTION: QString -``` -With most carrying a footer containing: -``` -LOGIN: QString -SESSION: QString -``` - ## Incoming Packages -##### Mod Vault - -- (deprecated) `{command: modvault, type: start}`: show the last 100 mods -- (deprecated) `{command: modvault, type: like, uid: }`: check if user liked the mod, otherwise increase the like counter -- (deprecated) `{command: modvault, type: download, uid: }`: notify server about a download (for download counter), does not start the download - ##### Social - `{command: social_add, friend|foe: }`: Add a friend or foe - `{command: social_remove, friend|foe: }`: Remove a friend or foe diff --git a/e2e_tests/fafclient.py b/e2e_tests/fafclient.py index 967d06b3a..ae2c253f8 100644 --- a/e2e_tests/fafclient.py +++ b/e2e_tests/fafclient.py @@ -4,7 +4,7 @@ from websockets.client import connect as ws_connect -from server.protocol import QDataStreamProtocol, SimpleJsonProtocol +from server.protocol import SimpleJsonProtocol from tests.integration_tests.conftest import read_until, read_until_command from .websocket_protocol import WebsocketProtocol @@ -32,7 +32,7 @@ async def close(self): await self.proto.close() - async def connect(self, host, port, protocol_class=QDataStreamProtocol): + async def connect(self, host, port, protocol_class=SimpleJsonProtocol): self.proto = protocol_class( *(await asyncio.open_connection(host, port)) ) @@ -104,8 +104,8 @@ async def login(self, username, password): "unique_id": unique_id }) msg = await self.read_until_command("welcome") - self.player_id = msg["id"] - self.player_name = msg["login"] + self.player_id = msg["me"]["id"] + self.player_name = msg["me"]["login"] return msg def get_unique_id(self, session): diff --git a/e2e_tests/test_login.py b/e2e_tests/test_login.py index dc187fd83..a9db7fa4b 100644 --- a/e2e_tests/test_login.py +++ b/e2e_tests/test_login.py @@ -9,6 +9,5 @@ async def test_user_existence(client_factory, username): """Verify that these users exist on the test server""" client, welcome_message = await client_factory.login(username, "foo") - assert welcome_message["login"] == welcome_message["me"]["login"] == username - assert welcome_message["id"] == welcome_message["me"]["id"] + assert welcome_message["me"]["login"] == username assert client.is_connected() diff --git a/server/__init__.py b/server/__init__.py index 777cc64c2..a5d63c734 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -108,7 +108,7 @@ from .oauth_service import OAuthService from .party_service import PartyService from .player_service import PlayerService -from .protocol import Protocol, QDataStreamProtocol +from .protocol import Protocol, SimpleJsonProtocol from .rating_service.rating_service import RatingService from .servercontext import ServerContext from .stats.game_stats_service import GameStatsService @@ -231,7 +231,7 @@ async def listen( self, address: tuple[str, int], name: Optional[str] = None, - protocol_class: type[Protocol] = QDataStreamProtocol, + protocol_class: type[Protocol] = SimpleJsonProtocol, proxy: bool = False, ) -> ServerContext: """ diff --git a/server/games/__init__.py b/server/games/__init__.py index 87c7603f5..5c7dd1044 100644 --- a/server/games/__init__.py +++ b/server/games/__init__.py @@ -13,7 +13,6 @@ GameConnectionState, GameState, GameType, - InitMode, ValidityState, Victory, VisibilityState @@ -39,7 +38,6 @@ class FeaturedMod(NamedTuple): "GameError", "GameState", "GameType", - "InitMode", "LadderGame", "ValidityState", "Victory", diff --git a/server/games/coop.py b/server/games/coop.py index e556de87a..67fe880ab 100644 --- a/server/games/coop.py +++ b/server/games/coop.py @@ -1,10 +1,9 @@ from .game import Game -from .typedefs import FA, GameType, InitMode, ValidityState, Victory +from .typedefs import FA, GameType, ValidityState, Victory class CoopGame(Game): """Class for coop game""" - init_mode = InitMode.NORMAL_LOBBY game_type = GameType.COOP def __init__(self, *args, **kwargs): diff --git a/server/games/custom_game.py b/server/games/custom_game.py index c71523bc4..efa1b1ecd 100644 --- a/server/games/custom_game.py +++ b/server/games/custom_game.py @@ -4,12 +4,11 @@ from server.rating import RatingType from .game import Game -from .typedefs import GameType, InitMode, ValidityState +from .typedefs import GameType, ValidityState @with_logger class CustomGame(Game): - init_mode = InitMode.NORMAL_LOBBY game_type = GameType.CUSTOM def __init__(self, id_, *args, **kwargs): diff --git a/server/games/game.py b/server/games/game.py index a489005e6..b44b7589a 100644 --- a/server/games/game.py +++ b/server/games/game.py @@ -37,7 +37,6 @@ GameConnectionState, GameState, GameType, - InitMode, ValidityState, Victory, VisibilityState @@ -52,7 +51,6 @@ class Game: """ Object that lasts for the lifetime of a game on FAF. """ - init_mode = InitMode.NORMAL_LOBBY game_type = GameType.CUSTOM def __init__( diff --git a/server/games/ladder_game.py b/server/games/ladder_game.py index 0e5ea8e0a..65014ad22 100644 --- a/server/games/ladder_game.py +++ b/server/games/ladder_game.py @@ -7,7 +7,7 @@ from .game import Game from .game_results import ArmyOutcome, GameOutcome -from .typedefs import GameState, GameType, InitMode +from .typedefs import GameState, GameType logger = logging.getLogger(__name__) @@ -23,8 +23,6 @@ def __init__(self, player: Player): class LadderGame(Game): """Class for 1v1 ladder games""" - - init_mode = InitMode.AUTO_LOBBY game_type = GameType.MATCHMAKER def __init__(self, id_, *args, **kwargs): diff --git a/server/games/typedefs.py b/server/games/typedefs.py index 8d8142f7d..8605004ff 100644 --- a/server/games/typedefs.py +++ b/server/games/typedefs.py @@ -22,12 +22,6 @@ class GameConnectionState(Enum): ENDED = 3 -@unique -class InitMode(Enum): - NORMAL_LOBBY = 0 - AUTO_LOBBY = 1 - - @unique class GameType(Enum): COOP = "coop" @@ -224,7 +218,6 @@ class FA(object): "GameConnectionState", "GameState", "GameType", - "InitMode", "TeamRatingSummary", "ValidityState", "Victory", diff --git a/server/ladder_service/ladder_service.py b/server/ladder_service/ladder_service.py index e7046493d..3f8767bdf 100644 --- a/server/ladder_service/ladder_service.py +++ b/server/ladder_service/ladder_service.py @@ -36,7 +36,7 @@ from server.decorators import with_logger from server.exceptions import DisabledError from server.game_service import GameService -from server.games import InitMode, LadderGame +from server.games import 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 @@ -529,7 +529,6 @@ def get_displayed_rating(player: Player) -> float: rating_type=queue.rating_type, max_players=len(all_players) ) - game.init_mode = InitMode.AUTO_LOBBY game.map_file_path = map_path game.set_name_unchecked(game_name(team1, team2)) @@ -642,13 +641,8 @@ async def launch_match( await game.wait_hosted(60) except asyncio.TimeoutError: raise NotConnectedError([host]) - finally: - # 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 - # already removed it from the queue. + try: # Launch the guests not_connected_guests = [ player for player in guests @@ -665,7 +659,7 @@ async def launch_match( is_host=False, options=make_game_options(guest) ) - try: + await game.wait_launched(60 + 10 * len(guests)) except asyncio.TimeoutError: connected_players = game.get_connected_players() diff --git a/server/lobbyconnection.py b/server/lobbyconnection.py index 245ed204e..9c227b315 100644 --- a/server/lobbyconnection.py +++ b/server/lobbyconnection.py @@ -4,10 +4,7 @@ import asyncio import contextlib -import json import random -import urllib.parse -import urllib.request from datetime import datetime from functools import wraps from typing import Optional @@ -46,7 +43,7 @@ Game, GameConnectionState, GameState, - InitMode, + GameType, VisibilityState ) from .geoip_service import GeoIpService @@ -140,7 +137,6 @@ async def ensure_authenticated(self, cmd): "Bottleneck", # sent by the game during reconnect "ask_session", "auth", - "create_account", "hello", "ping", "pong", @@ -222,9 +218,6 @@ async def command_ping(self, msg): async def command_pong(self, msg): pass - async def command_create_account(self, message): - raise ClientError("FAF no longer supports direct registration. Please use the website to register.", recoverable=True) - async def command_coop_list(self, message): """Request for coop map list""" async with self._db.acquire() as conn: @@ -527,14 +520,6 @@ async def command_auth(self, message): username = row.login - # DEPRECATED: IRC passwords are handled outside of the lobby server. - # This message remains here for backwards compatibility, but the data - # sent is meaningless and can be ignored by clients. - await self.send({ - "command": "irc_password", - "password": "deprecated" - }) - await self.on_player_login( player_id, username, unique_id, auth_method ) @@ -636,10 +621,6 @@ async def on_player_login( await self.send({ "command": "welcome", "me": self.player.to_dict(), - - # For backwards compatibility for old clients. For now. - "id": self.player.id, - "login": username }) # Tell player about everybody online. This must happen after "welcome". @@ -873,7 +854,7 @@ async def command_game_join(self, message): }) return - if game.init_mode != InitMode.NORMAL_LOBBY: + if game.game_type not in (GameType.CUSTOM, GameType.COOP): raise ClientError("The game cannot be joined in this way.") if game.password != password: @@ -888,9 +869,7 @@ async def command_game_join(self, message): @ice_only async def command_game_matchmaking(self, message): - queue_name = str( - message.get("queue_name") or message.get("mod", "ladder1v1") - ) + queue_name = str(message.get("queue_name", "ladder1v1")) state = str(message["state"]) if state == "stop": @@ -925,13 +904,6 @@ async def command_game_matchmaking(self, message): recoverable=True ) - # TODO: Remove this legacy behavior, use party instead - if "faction" in message: - party.set_factions( - self.player, - [Faction.from_value(message["faction"])] - ) - self.ladder_service.start_search( players, queue_name=queue_name, @@ -1054,8 +1026,6 @@ def _prepare_launch_game( # options are. Currently, options for ladder are hardcoded into the # client. "name": game.name, - # DEPRICATED: init_mode can be inferred from game_type - "init_mode": game.init_mode.value, "game_type": game.game_type.value, "rating_type": game.rating_type, **options._asdict() @@ -1063,80 +1033,6 @@ def _prepare_launch_game( return {k: v for k, v in cmd.items() if v is not None} - async def command_modvault(self, message): - type = message["type"] - - async with self._db.acquire() as conn: - if type == "start": - result = await conn.execute("SELECT uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon FROM table_mod ORDER BY likes DESC LIMIT 100") - - for row in result: - uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon = (row[i] for i in range(12)) - try: - link = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/" + filename) - thumbstr = "" - if icon: - thumbstr = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/mods_thumbs/" + urllib.parse.quote(icon)) - - out = dict(command="modvault_info", thumbnail=thumbstr, link=link, bugreports=[], - comments=[], description=description, played=played, likes=likes, - downloads=downloads, date=int(date.timestamp()), uid=uid, name=name, version=version, author=author, - ui=ui) - await self.send(out) - except Exception: - self._logger.error(f"Error handling table_mod row (uid: {uid})", exc_info=True) - - elif type == "like": - canLike = True - uid = message["uid"] - result = await conn.execute( - "SELECT uid, name, version, author, ui, date, downloads, " - "likes, played, description, filename, icon, likers FROM " - "`table_mod` WHERE uid = :uid LIMIT 1", - uid=uid - ) - - row = result.fetchone() - uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon, likerList = (row[i] for i in range(13)) - link = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/" + filename) - thumbstr = "" - if icon: - thumbstr = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/mods_thumbs/" + urllib.parse.quote(icon)) - - out = dict(command="modvault_info", thumbnail=thumbstr, link=link, bugreports=[], - comments=[], description=description, played=played, likes=likes + 1, - downloads=downloads, date=int(date.timestamp()), uid=uid, name=name, version=version, author=author, - ui=ui) - - try: - likers = json.loads(likerList) - if self.player.id in likers: - canLike = False - else: - likers.append(self.player.id) - except Exception: - likers = [] - - # TODO: Avoid sending all the mod info in the world just because we liked it? - if canLike: - await conn.execute( - "UPDATE mod_stats s " - "JOIN mod_version v ON v.mod_id = s.mod_id " - "SET s.likes = s.likes + 1, likers=:l WHERE v.uid=:id", - l=json.dumps(likers), - id=uid - ) - await self.send(out) - - elif type == "download": - uid = message["uid"] - await conn.execute( - "UPDATE mod_stats s " - "JOIN mod_version v ON v.mod_id = s.mod_id " - "SET downloads=downloads+1 WHERE v.uid = %s", uid) - else: - raise ValueError("invalid type argument") - # DEPRECATED: ICE servers are handled outside of the lobby server. # This message remains here for backwards compatibility, but the list # of servers will always be empty. diff --git a/server/matchmaker/matchmaker_queue.py b/server/matchmaker/matchmaker_queue.py index 778407981..089dc6412 100644 --- a/server/matchmaker/matchmaker_queue.py +++ b/server/matchmaker/matchmaker_queue.py @@ -288,8 +288,6 @@ def to_dict(self): ndigits=2 ), "num_players": self.num_players, - "boundary_80s": [search.boundary_80 for search in self._queue.keys()], - "boundary_75s": [search.boundary_75 for search in self._queue.keys()], # TODO: Remove, the client should query the API for this "team_size": self.team_size, } diff --git a/server/matchmaker/search.py b/server/matchmaker/search.py index 962b1b98b..eb0229943 100644 --- a/server/matchmaker/search.py +++ b/server/matchmaker/search.py @@ -1,6 +1,5 @@ import asyncio import itertools -import math import statistics import time from typing import Any, Callable, Optional @@ -112,25 +111,6 @@ def displayed_ratings(self) -> list[float]: """ return [rating.displayed() for rating in self.raw_ratings] - def _nearby_rating_range(self, delta: int) -> tuple[int, int]: - """ - Returns 'boundary' mu values for player matching. Adjust delta for - different game qualities. - """ - mu, _ = self.ratings[0] # Takes the rating of the first player, only works for 1v1 - rounded_mu = int(math.ceil(mu / 10) * 10) # Round to 10 - return rounded_mu - delta, rounded_mu + delta - - @property - def boundary_80(self) -> tuple[int, int]: - """ Achieves roughly 80% quality. """ - return self._nearby_rating_range(200) - - @property - def boundary_75(self) -> tuple[int, int]: - """ Achieves roughly 75% quality. FIXME - why is it MORE restrictive??? """ - return self._nearby_rating_range(100) - @property def failed_matching_attempts(self) -> int: return self._failed_matching_attempts diff --git a/server/servercontext.py b/server/servercontext.py index d6feb95af..36859213c 100644 --- a/server/servercontext.py +++ b/server/servercontext.py @@ -18,7 +18,7 @@ from .core import Service from .decorators import with_logger from .lobbyconnection import LobbyConnection -from .protocol import DisconnectedError, Protocol, QDataStreamProtocol +from .protocol import DisconnectedError, Protocol, SimpleJsonProtocol from .types import Address MiB = 2 ** 20 @@ -36,7 +36,7 @@ def __init__( name: str, connection_factory: Callable[[], LobbyConnection], services: Iterable[Service], - protocol_class: type[Protocol] = QDataStreamProtocol, + protocol_class: type[Protocol] = SimpleJsonProtocol, ): super().__init__() self.name = name diff --git a/tests/conftest.py b/tests/conftest.py index 8d550740e..cbcfffa5f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ CoopGame, FeaturedModType, Game, - InitMode, + GameType, ValidityState ) from server.geoip_service import GeoIpService @@ -241,7 +241,7 @@ def make_game(database, uid, players, game_type=Game): players.joining.getGame = mock.AsyncMock(return_value=game) players.peer.getGame = mock.AsyncMock(return_value=game) game.host = players.hosting - game.init_mode = InitMode.NORMAL_LOBBY + game.game_type = GameType.CUSTOM game.name = "Some game name" game.id = uid return game diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 7e0a69184..ab7683da6 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -502,7 +502,7 @@ async def connect_and_sign_in( session = await get_session(proto) await perform_login(proto, credentials) hello = await read_until_command(proto, "welcome", timeout=120) - player_id = hello["id"] + player_id = hello["me"]["id"] return player_id, session, proto diff --git a/tests/integration_tests/test_game.py b/tests/integration_tests/test_game.py index 8ca9f39e7..9df858378 100644 --- a/tests/integration_tests/test_game.py +++ b/tests/integration_tests/test_game.py @@ -137,10 +137,13 @@ async def open_fa(proto): async def start_search(proto, queue_name="ladder1v1"): + await proto.send_message({ + "command": "set_party_factions", + "factions": ["uef"] + }) await proto.send_message({ "command": "game_matchmaking", "state": "start", - "faction": "uef", "queue_name": queue_name }) return await read_until_command( @@ -152,7 +155,11 @@ async def start_search(proto, queue_name="ladder1v1"): ) -async def queue_player_for_matchmaking(user, lobby_server, queue_name="ladder1v1"): +async def queue_player_for_matchmaking( + user, + lobby_server, + queue_name="ladder1v1" +): player_id, _, proto = await connect_and_sign_in(user, lobby_server) await read_until_command(proto, "game_info") await start_search(proto, queue_name) @@ -160,27 +167,21 @@ async def queue_player_for_matchmaking(user, lobby_server, queue_name="ladder1v1 return player_id, proto -async def queue_players_for_matchmaking(lobby_server, queue_name: str = "ladder1v1"): +async def queue_players_for_matchmaking( + lobby_server, + queue_name: str = "ladder1v1" +): player1_id, proto1 = await queue_player_for_matchmaking( ("ladder1", "ladder1"), lobby_server, queue_name ) - player2_id, _, proto2 = await connect_and_sign_in( + player2_id, proto2 = await queue_player_for_matchmaking( ("ladder2", "ladder2"), - lobby_server + lobby_server, + queue_name ) - await read_until_command(proto2, "game_info") - - await proto2.send_message({ - "command": "game_matchmaking", - "state": "start", - "faction": 1, # Python client sends factions as numbers - "queue_name": queue_name - }) - await read_until_command(proto2, "search_info", state="start") - # If the players did not match, this will fail due to a timeout error await read_until_command(proto1, "match_found", timeout=30) await read_until_command(proto2, "match_found") diff --git a/tests/integration_tests/test_login.py b/tests/integration_tests/test_login.py index ee1e7c77b..cb67fe62f 100644 --- a/tests/integration_tests/test_login.py +++ b/tests/integration_tests/test_login.py @@ -50,7 +50,7 @@ async def test_server_ban_revoked_or_expired(lobby_server, user): msg = await proto.read_message() assert msg["command"] == "welcome" - assert msg["login"] == user + assert msg["me"]["login"] == user async def test_server_valid_login(lobby_server): @@ -79,8 +79,6 @@ async def test_server_valid_login(lobby_server): assert msg == { "command": "welcome", "me": me, - "id": 3, - "login": "Rhiza" } msg = await proto.read_message() assert msg == { @@ -124,8 +122,6 @@ async def test_server_valid_login_admin(lobby_server): assert msg == { "command": "welcome", "me": me, - "id": 1, - "login": "test" } msg = await proto.read_message() assert msg == { @@ -168,8 +164,6 @@ async def test_server_valid_login_moderator(lobby_server): assert msg == { "command": "welcome", "me": me, - "id": 20, - "login": "moderator" } msg = await proto.read_message() assert msg == { @@ -241,8 +235,6 @@ async def test_server_valid_login_with_token(lobby_server, jwk_priv_key, jwk_kid "unique_id": "some_id" }) - msg = await proto.read_message() - assert msg["command"] == "irc_password" msg = await proto.read_message() me = { "id": 3, @@ -266,8 +258,6 @@ async def test_server_valid_login_with_token(lobby_server, jwk_priv_key, jwk_kid assert msg == { "command": "welcome", "me": me, - "id": 3, - "login": "Rhiza" } msg = await proto.read_message() assert msg == { diff --git a/tests/integration_tests/test_matchmaker.py b/tests/integration_tests/test_matchmaker.py index 30ba1cbd9..835584146 100644 --- a/tests/integration_tests/test_matchmaker.py +++ b/tests/integration_tests/test_matchmaker.py @@ -51,7 +51,6 @@ async def test_game_launch_message(lobby_server): "uid": 41956, "mod": "ladder1v1", "name": "ladder1 Vs ladder2", - "init_mode": 1, "game_type": "matchmaker", "rating_type": "ladder_1v1", "team": 2, @@ -183,20 +182,11 @@ async def test_game_matchmaking_start_while_matched(lobby_server): async def test_game_matchmaking_timeout(lobby_server, game_service): _, proto1, _, proto2 = await queue_players_for_matchmaking(lobby_server) - msg1, msg2 = await asyncio.gather( - idle_response(proto1, timeout=120), - idle_response(proto2, timeout=120) - ) - # 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 `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 idle_response(proto1, timeout=120) await read_until_command(proto2, "match_cancelled", timeout=120) await read_until_command(proto1, "match_cancelled", timeout=120) - assert msg1["uid"] == msg2["uid"] assert msg1["mod"] == "ladder1v1" - assert msg2["mod"] == "ladder1v1" # Ensure that the game is cleaned up await read_until_command( @@ -207,7 +197,15 @@ async def test_game_matchmaking_timeout(lobby_server, game_service): ) assert game_service._games == {} - # Player's state is reset once they leave the game + # Player's state is not reset immediately + await proto1.send_message({ + "command": "game_matchmaking", + "state": "start", + }) + with pytest.raises(asyncio.TimeoutError): + await read_until_command(proto1, "search_info", state="start", timeout=5) + + # Player's state is only reset once they leave the game await proto1.send_message({ "command": "GameState", "target": "game", @@ -216,18 +214,15 @@ async def test_game_matchmaking_timeout(lobby_server, game_service): await proto1.send_message({ "command": "game_matchmaking", "state": "start", - "faction": "uef" }) await read_until_command(proto1, "search_info", state="start", timeout=5) - # And not before they've left the game + # But it is reset for the player who didn't make it into the game await proto2.send_message({ "command": "game_matchmaking", "state": "start", - "faction": "uef" }) - with pytest.raises(asyncio.TimeoutError): - await read_until_command(proto2, "search_info", state="start", timeout=5) + await read_until_command(proto2, "search_info", state="start", timeout=5) @fast_forward(120) @@ -443,14 +438,12 @@ async def test_matchmaker_info_message(lobby_server, mocker): for queue in msg["queues"]: assert "queue_name" in queue assert "team_size" in queue - assert "num_players" in queue assert queue["queue_pop_time"] == "2019-07-01T16:53:21+00:00" assert queue["queue_pop_time_delta"] == math.ceil( config.QUEUE_POP_TIME_MAX / 2 ) - assert queue["boundary_80s"] == [] - assert queue["boundary_75s"] == [] + assert queue["num_players"] == 0 @fast_forward(10) @@ -477,14 +470,12 @@ async def test_command_matchmaker_info(lobby_server, mocker): for queue in msg["queues"]: assert "queue_name" in queue assert "team_size" in queue - assert "num_players" in queue assert queue["queue_pop_time"] == "2019-07-01T16:53:21+00:00" assert queue["queue_pop_time_delta"] == math.ceil( config.QUEUE_POP_TIME_MAX / 2 ) - assert queue["boundary_80s"] == [] - assert queue["boundary_75s"] == [] + assert queue["num_players"] == 0 @fast_forward(10) @@ -511,10 +502,10 @@ async def read_update_msg(): queue_message = next( q for q in msg["queues"] if q["queue_name"] == "ladder1v1" ) - if not queue_message["boundary_80s"]: + if queue_message["num_players"] == 0: continue - assert len(queue_message["boundary_80s"]) == 1 + assert queue_message["num_players"] == 1 return @@ -529,7 +520,7 @@ async def read_update_msg(): msg = await read_until_command(proto, "matchmaker_info") queue_message = next(q for q in msg["queues"] if q["queue_name"] == "ladder1v1") - assert len(queue_message["boundary_80s"]) == 0 + assert queue_message["num_players"] == 0 @fast_forward(10) diff --git a/tests/integration_tests/test_modvault.py b/tests/integration_tests/test_modvault.py deleted file mode 100644 index d615dd8c5..000000000 --- a/tests/integration_tests/test_modvault.py +++ /dev/null @@ -1,55 +0,0 @@ -from .conftest import connect_and_sign_in, read_until_command - - -async def test_modvault_start(lobby_server): - _, _, proto = await connect_and_sign_in( - ("test", "test_password"), - lobby_server - ) - - await read_until_command(proto, "game_info") - - await proto.send_message({ - "command": "modvault", - "type": "start" - }) - - # Make sure all 5 mod version messages are sent - for _ in range(5): - await read_until_command(proto, "modvault_info") - - -async def test_modvault_like(lobby_server): - _, _, proto = await connect_and_sign_in( - ("test", "test_password"), - lobby_server - ) - - await read_until_command(proto, "game_info") - - await proto.send_message({ - "command": "modvault", - "type": "like", - "uid": "FFF" - }) - - msg = await read_until_command(proto, "modvault_info") - # Not going to verify the date - del msg["date"] - - assert msg == { - "command": "modvault_info", - "thumbnail": "", - "link": "http://content.faforever.com/faf/vault/noicon.zip", - "bugreports": [], - "comments": [], - "description": "The best version so far", - "played": 0, - "likes": 1.0, - "downloads": 0, - "uid": "FFF", - "name": "Mod without icon", - "version": 1, - "author": "foo", - "ui": 1 - } diff --git a/tests/integration_tests/test_server_instance.py b/tests/integration_tests/test_server_instance.py index 1b805d6a8..669672d71 100644 --- a/tests/integration_tests/test_server_instance.py +++ b/tests/integration_tests/test_server_instance.py @@ -1,6 +1,5 @@ from server import ServerInstance from server.config import config -from server.protocol import QDataStreamProtocol, SimpleJsonProtocol from tests.utils import fast_forward from .conftest import connect_and_sign_in, read_until @@ -52,33 +51,23 @@ async def test_multiple_contexts( ) broadcast_service.server = instance - await instance.listen(("127.0.0.1", 8111), QDataStreamProtocol) - await instance.listen(("127.0.0.1", 8112), SimpleJsonProtocol) + await instance.listen(("127.0.0.1", 8111)) + await instance.listen(("127.0.0.1", 8112)) ctx_1, ctx_2 = tuple(instance.contexts) - if ctx_1.protocol_class is SimpleJsonProtocol: - ctx_1, ctx_2 = ctx_2, ctx_1 # Connect one client to each context _, _, proto1 = await connect_and_sign_in( - await tmp_user("QDataStreamUser"), ctx_1 + await tmp_user("User"), ctx_1 ) _, _, proto2 = await connect_and_sign_in( - await tmp_user("SimpleJsonUser"), ctx_2 + await tmp_user("User"), ctx_2 ) # Verify that the users can see each other - await read_until( - proto1, - lambda m: has_player(m, "SimpleJsonUser1"), - timeout=5 - ) - await read_until( - proto2, - lambda m: has_player(m, "QDataStreamUser1"), - timeout=5 - ) + await read_until(proto1, lambda m: has_player(m, "User1"), timeout=5) + await read_until(proto2, lambda m: has_player(m, "User2"), timeout=5) # Host a game game_id = await host_game(proto1) diff --git a/tests/integration_tests/test_servercontext.py b/tests/integration_tests/test_servercontext.py index f4d4885e7..eafb97dc1 100644 --- a/tests/integration_tests/test_servercontext.py +++ b/tests/integration_tests/test_servercontext.py @@ -8,7 +8,7 @@ from server import ServerContext from server.core import Service from server.lobbyconnection import LobbyConnection -from server.protocol import DisconnectedError, QDataStreamProtocol +from server.protocol import DisconnectedError, SimpleJsonProtocol from tests.utils import exhaust_callbacks, fast_forward @@ -77,7 +77,7 @@ async def test_serverside_abort( srv, ctx = mock_context reader, writer = await asyncio.open_connection(*srv.sockets[0].getsockname()) with closing(writer): - proto = QDataStreamProtocol(reader, writer) + proto = SimpleJsonProtocol(reader, writer) await proto.send_message({"some_junk": True}) await exhaust_callbacks(event_loop) diff --git a/tests/integration_tests/test_teammatchmaker.py b/tests/integration_tests/test_teammatchmaker.py index e07d795ef..f2445730e 100644 --- a/tests/integration_tests/test_teammatchmaker.py +++ b/tests/integration_tests/test_teammatchmaker.py @@ -41,11 +41,17 @@ async def queue_players_for_matchmaking(lobby_server): read_until_command(proto, "game_info") for proto in protos ]) + await asyncio.gather(*[ + proto.send_message({ + "command": "set_party_factions", + "factions": ["uef"] + }) + for proto in protos + ]) await asyncio.gather(*[ proto.send_message({ "command": "game_matchmaking", "state": "start", - "faction": "uef", "queue_name": "tmm2v2" }) for proto in protos @@ -76,19 +82,19 @@ async def test_info_message(lobby_server): "command": "game_matchmaking", "state": "start", "faction": "uef", - "mod": "tmm2v2" + "queue_name": "tmm2v2" }) msg = await read_until_command(proto, "matchmaker_info") assert msg["queues"] for queue in msg["queues"]: - boundaries = queue["boundary_80s"] + num_players = queue["num_players"] if queue["queue_name"] == "tmm2v2": - assert boundaries == [[300, 700]] + assert num_players == 1 else: - assert boundaries == [] + assert num_players == 0 @fast_forward(10) @@ -100,7 +106,7 @@ async def test_game_matchmaking(lobby_server): uid = set(msg["uid"] for msg in msgs) assert len(uid) == 1 for msg in msgs: - assert msg["init_mode"] == 1 + assert msg["game_type"] == "matchmaker" assert "None" not in msg["name"] assert msg["mod"] == "faf" assert msg["expected_players"] == 4 @@ -117,18 +123,27 @@ async def test_game_matchmaking_multiqueue(lobby_server): read_until_command(proto, "game_info") for proto in protos ]) + await protos[0].send_message({ + "command": "set_party_factions", + "factions": ["uef"] + }) await protos[0].send_message({ "command": "game_matchmaking", "state": "start", - "faction": "uef", "queue_name": "ladder1v1" }) await read_until_command(protos[0], "search_info", state="start") + await asyncio.gather(*[ + proto.send_message({ + "command": "set_party_factions", + "factions": ["aeon"] + }) + for proto in protos + ]) await asyncio.gather(*[ proto.send_message({ "command": "game_matchmaking", "state": "start", - "faction": "aeon", "queue_name": "tmm2v2" }) for proto in protos @@ -145,7 +160,7 @@ async def test_game_matchmaking_multiqueue(lobby_server): uid = set(msg["uid"] for msg in msgs) assert len(uid) == 1 for msg in msgs: - assert msg["init_mode"] == 1 + assert msg["game_type"] == "matchmaker" assert "None" not in msg["name"] assert msg["mod"] == "faf" assert msg["expected_players"] == 4 @@ -225,7 +240,7 @@ async def test_game_matchmaking_with_parties(lobby_server): uid = set(msg["uid"] for msg in msgs) assert len(uid) == 1 for i, msg in enumerate(msgs): - assert msg["init_mode"] == 1 + assert msg["game_type"] == "matchmaker" assert "None" not in msg["name"] assert msg["mod"] == "faf" assert msg["expected_players"] == 4 @@ -299,7 +314,7 @@ async def test_newbie_matchmaking_with_parties(lobby_server): uid = set(msg["uid"] for msg in msgs) assert len(uid) == 1 for msg in msgs: - assert msg["init_mode"] == 1 + assert msg["game_type"] == "matchmaker" assert "None" not in msg["name"] assert msg["mod"] == "faf" assert msg["expected_players"] == 4 @@ -318,7 +333,6 @@ async def test_game_matchmaking_multiqueue_timeout(lobby_server): await protos[0].send_message({ "command": "game_matchmaking", "state": "start", - "faction": "cybran", "queue_name": "ladder1v1" }) await read_until_command(protos[0], "search_info", state="start") @@ -326,7 +340,6 @@ async def test_game_matchmaking_multiqueue_timeout(lobby_server): proto.send_message({ "command": "game_matchmaking", "state": "start", - "faction": "seraphim", "queue_name": "tmm2v2" }) for proto in protos @@ -346,6 +359,14 @@ async def test_game_matchmaking_multiqueue_timeout(lobby_server): # We don't send the `GameState: Lobby` command so the game should time out await read_until_command(protos[0], "match_cancelled", timeout=120) + # Player's state is not reset immediately + await protos[0].send_message({ + "command": "game_matchmaking", + "state": "start", + }) + with pytest.raises(asyncio.TimeoutError): + await read_until_command(protos[1], "search_info", state="start", timeout=5) + # Player's state is reset once they leave the game await protos[0].send_message({ "command": "GameState", @@ -355,7 +376,6 @@ async def test_game_matchmaking_multiqueue_timeout(lobby_server): await protos[0].send_message({ "command": "game_matchmaking", "state": "start", - "faction": "uef" }) await read_until_command( protos[0], @@ -409,7 +429,6 @@ async def test_game_matchmaking_multiqueue_multimatch(lobby_server): proto.send_message({ "command": "game_matchmaking", "state": "start", - "faction": "uef", "queue_name": "ladder1v1" }) for proto in protos[:2] @@ -418,7 +437,6 @@ async def test_game_matchmaking_multiqueue_multimatch(lobby_server): proto.send_message({ "command": "game_matchmaking", "state": "start", - "faction": "aeon", "queue_name": "tmm2v2" }) for proto in protos @@ -475,7 +493,6 @@ async def test_game_matchmaking_timeout(lobby_server): await protos[0].send_message({ "command": "game_matchmaking", "state": "start", - "faction": "uef" }) await read_until_command( protos[0], @@ -604,7 +621,7 @@ async def test_ratings_initialized_based_on_global(lobby_server): "command": "game_matchmaking", "state": "start", "faction": "uef", - "mod": "tmm2v2" + "queue_name": "tmm2v2" }) # Need to connect another user to guarantee triggering a message containing @@ -671,7 +688,7 @@ async def test_ratings_initialized_based_on_global_persisted( await proto.send_message({ "command": "game_matchmaking", "state": "start", - "mod": "tmm2v2" + "queue_name": "tmm2v2" }) await read_until_command(proto, "search_info") @@ -771,7 +788,7 @@ async def test_party_cleanup_on_abort(lobby_server): await proto.send_message({ "command": "game_matchmaking", "state": "start", - "mod": "tmm2v2" + "queue_name": "tmm2v2" }) # The queue was successful. This would time out on failure. await read_until_command(proto, "search_info", state="start") diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 7d65cdbb7..83e67f909 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -10,7 +10,7 @@ from server.games import Game from server.ladder_service import LadderService from server.ladder_service.violation_service import ViolationService -from server.protocol import QDataStreamProtocol +from server.protocol import SimpleJsonProtocol @pytest.fixture(scope="session") @@ -87,7 +87,7 @@ def game_connection( database=database, game=game, player=players.hosting, - protocol=mock.create_autospec(QDataStreamProtocol), + protocol=mock.create_autospec(SimpleJsonProtocol), player_service=player_service, games=game_service ) diff --git a/tests/unit_tests/test_ladder_service.py b/tests/unit_tests/test_ladder_service.py index b62b063a3..c2393a818 100644 --- a/tests/unit_tests/test_ladder_service.py +++ b/tests/unit_tests/test_ladder_service.py @@ -159,7 +159,6 @@ async def test_start_game_1v1( game = game_service[game_service.game_id_counter] assert player1.lobby_connection.write_launch_game.called - # TODO: Once client supports `match_cancelled` change this to `assert not` assert player2.lobby_connection.write_launch_game.called assert isinstance(game, LadderGame) assert game.rating_type == queue.rating_type @@ -239,8 +238,7 @@ async def test_start_game_timeout( "game_id": 41956 }) assert p1.lobby_connection.write_launch_game.called - # TODO: Once client supports `match_cancelled` change this to `assert not` - assert p2.lobby_connection.write_launch_game.called + assert not p2.lobby_connection.write_launch_game.called assert p1.state is PlayerState.IDLE assert p2.state is PlayerState.IDLE @@ -357,8 +355,7 @@ async def test_start_game_game_closed_by_host( "game_id": 41956 }) assert p1.lobby_connection.write_launch_game.called - # TODO: Once client supports `match_cancelled` change this to `assert not` - assert p2.lobby_connection.write_launch_game.called + assert not p2.lobby_connection.write_launch_game.called assert p1.state is PlayerState.IDLE assert p2.state is PlayerState.IDLE diff --git a/tests/unit_tests/test_lobbyconnection.py b/tests/unit_tests/test_lobbyconnection.py index 3714dd75b..dc6f5f5c3 100644 --- a/tests/unit_tests/test_lobbyconnection.py +++ b/tests/unit_tests/test_lobbyconnection.py @@ -11,7 +11,7 @@ from server.exceptions import BanError, ClientError from server.game_service import GameService from server.gameconnection import GameConnection -from server.games import CustomGame, Game, GameState, InitMode, VisibilityState +from server.games import CustomGame, Game, GameState, GameType, VisibilityState from server.geoip_service import GeoIpService from server.ladder_service import LadderService from server.lobbyconnection import LobbyConnection @@ -20,7 +20,7 @@ from server.party_service import PartyService from server.player_service import PlayerService from server.players import PlayerState -from server.protocol import DisconnectedError, QDataStreamProtocol +from server.protocol import DisconnectedError, SimpleJsonProtocol from server.rating import InclusiveRange, RatingType from server.team_matchmaker import PlayerParty from server.types import Address @@ -69,7 +69,7 @@ def mock_games(): @pytest.fixture def mock_protocol(): - return mock.create_autospec(QDataStreamProtocol(mock.Mock(), mock.Mock())) + return mock.create_autospec(SimpleJsonProtocol(mock.Mock(), mock.Mock())) @pytest.fixture @@ -170,21 +170,6 @@ async def test_command_pong_does_nothing(lobbyconnection): lobbyconnection.send.assert_not_called() -async def test_command_create_account_returns_error(lobbyconnection): - lobbyconnection.send = mock.AsyncMock() - - await lobbyconnection.on_message_received({ - "command": "create_account" - }) - - lobbyconnection.send.assert_called_once_with({ - "command": "notice", - "style": "error", - "text": ("FAF no longer supports direct registration. " - "Please use the website to register.") - }) - - async def test_double_login(lobbyconnection, mock_players, player_factory): lobbyconnection.check_policy_conformity = mock.AsyncMock(return_value=True) old_player = player_factory(lobby_connection_spec="auto") @@ -320,7 +305,6 @@ async def test_command_game_join_calls_join_game( "uid": 42, "mod": "faf", "name": "Test Game Name", - "init_mode": InitMode.NORMAL_LOBBY.value, "game_type": "custom", "rating_type": "global", } @@ -360,7 +344,6 @@ async def test_command_game_join_uid_as_str( "mod": "faf", "uid": 42, "name": "Test Game Name", - "init_mode": InitMode.NORMAL_LOBBY.value, "game_type": "custom", "rating_type": "global", } @@ -379,7 +362,7 @@ async def test_command_game_join_without_password( lobbyconnection.game_service = game_service game = mock.create_autospec(Game) game.state = GameState.LOBBY - game.init_mode = InitMode.NORMAL_LOBBY + game.game_type = GameType.CUSTOM game.password = "password" game.game_mode = "faf" game.id = 42 @@ -424,7 +407,7 @@ async def test_command_game_join_game_not_found( }) -async def test_command_game_join_game_bad_init_mode( +async def test_command_game_join_matchmaker_game( lobbyconnection, game_service, test_game_info, @@ -434,7 +417,7 @@ async def test_command_game_join_game_bad_init_mode( lobbyconnection.game_service = game_service game = mock.create_autospec(Game) game.state = GameState.LOBBY - game.init_mode = InitMode.AUTO_LOBBY + game.game_type = GameType.MATCHMAKER game.id = 42 game.host = players.hosting game_service._games[42] = game @@ -991,8 +974,6 @@ async def test_command_matchmaker_info( "queue_pop_time_delta": 1.0, "team_size": 1, "num_players": 6, - "boundary_80s": [(1800, 2200), (300, 700), (800, 1200)], - "boundary_75s": [(1900, 2100), (400, 600), (900, 1100)] } ] }) diff --git a/tests/unit_tests/test_matchmaker_queue.py b/tests/unit_tests/test_matchmaker_queue.py index db058bc11..ac15699be 100644 --- a/tests/unit_tests/test_matchmaker_queue.py +++ b/tests/unit_tests/test_matchmaker_queue.py @@ -182,15 +182,6 @@ def test_search_no_match_wrong_type(matchmaker_players): assert not s1.matches_with(42) -def test_search_boundaries(matchmaker_players): - p1 = matchmaker_players[0] - s1 = Search([p1]) - assert p1.ratings[RatingType.LADDER_1V1][0] > s1.boundary_80[0] - assert p1.ratings[RatingType.LADDER_1V1][0] < s1.boundary_80[1] - assert p1.ratings[RatingType.LADDER_1V1][0] > s1.boundary_75[0] - assert p1.ratings[RatingType.LADDER_1V1][0] < s1.boundary_75[1] - - def test_search_expansion_controlled_by_failed_matching_attempts(matchmaker_players): p1 = matchmaker_players[1] s1 = Search([p1]) diff --git a/tests/unit_tests/test_protocol.py b/tests/unit_tests/test_protocol.py index b9596d8d3..af9d36023 100644 --- a/tests/unit_tests/test_protocol.py +++ b/tests/unit_tests/test_protocol.py @@ -15,14 +15,22 @@ ) +@pytest.fixture( + scope="session", + params=(QDataStreamProtocol, SimpleJsonProtocol) +) +def protocol_class(request): + return request.param + + @pytest.fixture(scope="session") -def qstream_protocol_context(): +def protocol_context(protocol_class): @asynccontextmanager async def make_protocol(): rsock, wsock = socketpair() with closing(wsock): reader, writer = await asyncio.open_connection(sock=rsock) - proto = QDataStreamProtocol(reader, writer) + proto = protocol_class(reader, writer) yield proto await proto.close() @@ -64,9 +72,9 @@ async def qstream_protocol(reader, writer): await proto.close() -@pytest.fixture(params=(QDataStreamProtocol, SimpleJsonProtocol)) -async def protocol(request, reader, writer): - proto = request.param(reader, writer) +@pytest.fixture +async def protocol(protocol_class, reader, writer): + proto = protocol_class(reader, writer) yield proto await proto.close() @@ -88,7 +96,7 @@ async def do_nothing(client_reader, client_writer): @pytest.fixture async def unix_protocol(unix_srv): (reader, writer) = await asyncio.open_unix_connection("/tmp/test.sock") - proto = QDataStreamProtocol(reader, writer) + proto = SimpleJsonProtocol(reader, writer) yield proto await proto.close() @@ -122,12 +130,12 @@ async def test_QDataStreamProtocol_recv_small_message(qstream_protocol, reader): assert message == {"some_header": True, "legacy": ["Goodbye"]} -async def test_QDataStreamProtocol_recv_malformed_message(qstream_protocol, reader): +async def test_recv_malformed_message(protocol, reader): reader.feed_data(b"\0") reader.feed_eof() - with pytest.raises(asyncio.IncompleteReadError): - await qstream_protocol.read_message() + with pytest.raises((asyncio.IncompleteReadError, json.JSONDecodeError)): + await protocol.read_message() async def test_QDataStreamProtocol_recv_large_array(qstream_protocol, reader): @@ -157,15 +165,14 @@ async def test_QDataStreamProtocol_unpacks_evil_qstring(qstream_protocol, reader "Message": ["message", 10], "with": 1000 }) +@example(message={ + "some_header": True, + "array": [str(i) for i in range(1520)] +}) @settings(max_examples=300) -async def test_QDataStreamProtocol_pack_unpack( - qstream_protocol_context, - message -): - async with qstream_protocol_context() as protocol: - protocol.reader.feed_data( - QDataStreamProtocol.pack_message(json.dumps(message)) - ) +async def test_pack_unpack(protocol_context, message): + async with protocol_context() as protocol: + protocol.reader.feed_data(protocol.encode_message(message)) assert message == await protocol.read_message() @@ -176,19 +183,10 @@ async def test_QDataStreamProtocol_pack_unpack( "Message": ["message", 10], "with": 1000 }) -async def test_QDataStreamProtocol_deterministic(message): - assert ( - QDataStreamProtocol.encode_message(message) == - QDataStreamProtocol.encode_message(message) == - QDataStreamProtocol.encode_message(message) - ) - - -async def test_QDataStreamProtocol_encode_ping_pong(): - assert QDataStreamProtocol.encode_message({"command": "ping"}) == \ - b"\x00\x00\x00\x0c\x00\x00\x00\x08\x00P\x00I\x00N\x00G" - assert QDataStreamProtocol.encode_message({"command": "pong"}) == \ - b"\x00\x00\x00\x0c\x00\x00\x00\x08\x00P\x00O\x00N\x00G" +async def test_deterministic(protocol_class, message): + bytes1 = protocol_class.encode_message(message) + bytes2 = protocol_class.encode_message(message) + assert bytes1 == bytes2 async def test_send_message_simultaneous_writes(unix_protocol):