From 4be32b232c189d9abee37405810e0e1e3438c0e3 Mon Sep 17 00:00:00 2001 From: Askaholic Date: Fri, 14 Mar 2025 15:27:12 -0400 Subject: [PATCH] Ignore duplicate 'Idle' and 'Lobby' messages These are likely caused by a bug in the client launching multiple FA instances. In this case we just ignore the extra messages and assume that the player will close one of the FA processes shortly after launch. --- server/gameconnection.py | 14 +++++++++ tests/integration_tests/test_game.py | 42 +++++++++++++++++++++++++ tests/unit_tests/test_gameconnection.py | 21 +++++++++++-- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/server/gameconnection.py b/server/gameconnection.py index 2f3d9f6b1..73774be53 100644 --- a/server/gameconnection.py +++ b/server/gameconnection.py @@ -114,6 +114,13 @@ async def _handle_idle_state(self): """ assert self.game + if self.state is not GameConnectionState.INITIALIZING: + self._logger.warning( + "Ignoring unexpected 'Idle' state when state was %s", + self.state, + ) + return + if self.player == self.game.host: self.game.state = GameState.LOBBY self._state = GameConnectionState.CONNECTED_TO_HOST @@ -132,6 +139,13 @@ async def _handle_lobby_state(self): """ player_state = self.player.state if player_state == PlayerState.HOSTING: + if self.game.hosted_at is not None: + self._logger.warning( + "Ignoring unexpected 'Lobby' state sent when the game was " + "already hosted.", + ) + return + await self.send_HostGame(self.game.map.folder_name) self.game.set_hosted() # If the player is joining, we connect him to host diff --git a/tests/integration_tests/test_game.py b/tests/integration_tests/test_game.py index 3c90ecb45..62dfb51e7 100644 --- a/tests/integration_tests/test_game.py +++ b/tests/integration_tests/test_game.py @@ -1320,6 +1320,48 @@ async def test_ladder_game_not_joinable(lobby_server): } +@fast_forward(60) +async def test_gamestate_lobby_double_game_instance(lobby_server): + """There was a bug in the client that was causing multiple game instances + to launch simultaneously. This could cause the GpgNet messages in the game + startup sequence to be sent twice. + """ + _, _, host_proto = await connect_and_sign_in( + ("test", "test_password"), lobby_server + ) + _, _, guest_proto = await connect_and_sign_in( + ("Rhiza", "puff_the_magic_dragon"), lobby_server + ) + await read_until_command(host_proto, "game_info") + await read_until_command(guest_proto, "game_info") + + await host_proto.send_message({ + "command": "game_host", + "mod": "faf", + "visibility": "public", + }) + msg = await read_until_command(host_proto, "game_launch") + game_id = int(msg["uid"]) + + for _ in range(2): + await host_proto.send_message({ + "target": "game", + "command": "GameState", + "args": ["Idle"] + }) + + for _ in range(2): + await host_proto.send_message({ + "target": "game", + "command": "GameState", + "args": ["Lobby"] + }) + + await read_until_command(host_proto, "HostGame", target="game") + + await join_game(guest_proto, game_id) + + @pytest.mark.flaky @fast_forward(60) async def test_gamestate_ended_clears_references( diff --git a/tests/unit_tests/test_gameconnection.py b/tests/unit_tests/test_gameconnection.py index f39b9facb..06821e838 100644 --- a/tests/unit_tests/test_gameconnection.py +++ b/tests/unit_tests/test_gameconnection.py @@ -99,18 +99,33 @@ async def test_connect_to_peer_disconnected(game_connection): await game_connection.connect_to_peer(peer) -async def test_handle_action_GameState_idle_adds_connection( +async def test_handle_action_GameState_idle_hosting( game: Game, game_connection: GameConnection, players ): - players.joining.game = game game_connection.player = players.hosting game_connection.game = game await game_connection.handle_action("GameState", ["Idle"]) - game.add_game_connection.assert_called_with(game_connection) + game.add_game_connection.assert_called_once_with(game_connection) + + +async def test_handle_action_GameState_idle_joining( + game: Game, + game_connection: GameConnection, + players +): + game_connection.player = players.joining + game_connection.game = game + + await game_connection.handle_action("GameState", ["Idle"]) + + assert players.joining.state == PlayerState.JOINING + assert game_connection.state == GameConnectionState.INITIALIZED + + game.add_game_connection.assert_not_called() async def test_handle_action_GameState_idle_sets_player_state(