Hastwell / Omukade.Cheyenne

Pokemon TCG Live (Rainier) 3rd Party Server
GNU Affero General Public License v3.0
10 stars 2 forks source link

Game client will send OpponentMatchTimeOut in some cases #16

Open Hill-98 opened 5 months ago

Hill-98 commented 5 months ago

The game client will send an OpponentMatchTimeOut message to the server under certain circumstances. But the timer didn't time out.

I'm not sure under what circumstances the game will send it, but if the game client is on HOME and then the server is restarted, it will most likely keep sending OpponentMatchTimeOut in the next game.

When I implemented the timer myself before, I completely ignored the processing of OpponentMatchTimeOut and OpponentOperationTimeOut messages. Instead, I manually checked the remaining game time after each message was processed.

void CheckPlayerTimeout(GameStateOmukade gameState)
{
    if (gameState?.CurrentOperation == null)
    {
        return;
    }
    var p1Timer = gameState.CurrentOperation.p1MatchTimeRemaining;
    var p2Timer = gameState.CurrentOperation.p2MatchTimeRemaining;
    if (p1Timer <= 0f)
    {
        ForcePlayerToQuit(gameState.player1metadata!);
    }
    else if (p2Timer <= 0f)
    {
        ForcePlayerToQuit(gameState.player2metadata!);
    }
}

void HandleRainierGameMessage(PlayerMetadata player, GameMessage gm)
{
    // ...
            switch (smg.messageType)
            {
                case MessageType.MatchOperation:
                case MessageType.MatchInput:
                case MessageType.MatchInputUpdate:
                // case MessageType.ChangeCoinState:
                // case MessageType.ChangeDeckOrder:
                    OfflineAdapter.ReceiveOperation(player.PlayerId, currentGame, smg);
                    break;
                case MessageType.SendEmote:
                    string? rawPayload = smg.compressedValue == null ? null : Compression.Unzip(smg.compressedValue);
                    if (rawPayload == null) throw new ArgumentNullException("Emote payload is null");

                    string? emoteName = JsonConvert.DeserializeObject<string>(rawPayload);

                    if (emoteName == null) throw new ArgumentNullException("Cannot send a null emote");

                    foreach (SharedSDKUtils.PlayerInfo playerInGame in currentGame.playerInfos)
                    {
                        // Don't send our own emote to ourselves
                        if (playerInGame.playerID == player.PlayerId) continue;

                        SendPacketToClient(UserMetadata.GetValueOrDefault(playerInGame.playerID), new ServerMessage(MessageType.SendEmote, emoteName, playerInGame.playerID, smg.operationID, currentGame.matchId).AsPlayerMessage());
                    }
                    break;
                case MessageType.MatchReadyTimeOut:
                    // concede this player
                    ForcePlayerToQuit(player);
                    break;
                case MessageType.OpponentMatchTimeOut:
                case MessageType.OpponentOperationTimeOut:
                    break;
                default:
                    throw new NotImplementedException($"Unsupported Game Message type received from client :: {smg.messageType}");
            }

            CheckPlayerTimeout(currentGame);
}
Hastwell commented 5 months ago

I'm curious on the timing of the OpponentMatchTimeout messages. In Vanilla, server maintenance always takes many hours so no person would ever leave the client running for that long, but Cheyenne maintenance cycles can be significantly shorter, enough that leftover timers could still be running.

In a game, the client sends OpponentMatchTimeOut based on a timer task started upon receipt of each message. When a new message is received, the previous task is cancelled and a new one started. It may be possible that one of these is left over for whatever reason and randomly sends OpponentMatchTimeOut when the timer eventually expires.

Question time! Were affected players experiencing this bug...

  1. In a game when the server was restarted?
  2. Previously in a game that completed, then went to the home screen, then the server restarted?
  3. During the maintenance, informed the game needs to restart (which does this by rebooting Unity and in theory stopping ongoing timers and tasks), or did the game keep running?
  4. Sending OpponentMatchTimeOut immediately upon joining a game, or after some time elapsed? This would manifest as the player's opponent conceding due to timeout.
  5. Does the timer expire around when the player's previous game would have timed out, had the timer kept running the entire time?

You may not know the answer to all of these questions, but each answer you do provide allows me to form a working theory.

Hill-98 commented 5 months ago
  1. Restarting the server during the game will cause the game to terminate, so this will not be done.
  2. Yes, I played the game once, go to the home screen, restarted the server and played the game again.
  3. If the server restart interval exceeds a certain time, the game client will notify that it needs to reconnect to the server, but the restart time of my development environment is short enough, so it does not prompt.
  4. Some are sent immediately after joining the game, while others are sent after a few seconds.
  5. I haven't done this test with 1500 seconds, but I think I can test it with a shorter time tomorrow.
Hill-98 commented 5 months ago

I'm not sure about the third question because I'm using Visual Studio's "Restart" button in a development environment.