Changeset 25156

Timestamp:
Mar 29, 2021, 9:53:06 AM (3 years ago)
Author:
wraitii
Message:

Netcode: allow observers to lag behind the live game.

Observers no longer lag the game for players. There is still some time to serialise the game when sending it to a joining observer, and depending on the chosen 'max lag' the game may stop while observers sufficiently catch up, but this impact too is reduced.

  • Make the NetServerTurnManager ignore players marked as 'observers' for the purpose of ending a turn, effectively making it possible for observers to lag without it affecting the players in any way.
  • Add a config option (network.observermaxlag) that specifies how many turns behind the live game observers are allowed to be. Default to 10 turns, or 2 seconds, to keep them 'largely live'.
  • The controller is not treated as an observer.
  • Implement a simple UI to show this delay & allow the game to speed up automatically to try and catch up. This can be deactivated via network.autocatchup.
  • Move network options to the renamed 'Network / Lobby' options page.
  • Do not debug_warn/crash when receiving commands from the past - instead warn and carry on, to avoid DOS and "coop play" issues.

Refs #5903, Refs #4210

Differential Revision: https://code.wildfiregames.com/D3737

Location:
ps/trunk
Files:
2 added
10 edited

Legend:

Unmodified
Added
Removed
  • ps/trunk/binaries/data/config/default.cfg

    r25123 r25156  
    487487lateobservers = everyone          ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled.
    488488observerlimit = 8                 ; Prevent further observer joins in running games if this limit is reached
     489
     490
    489491
    490492[overlay]
  • ps/trunk/binaries/data/mods/public/gui/options/options.json

    r25116 r25156  
    3030            {
    3131                "type": "boolean",
    32                 "label": "Network warnings",
    33                 "tooltip": "Show which player has a bad connection in multiplayer games.",
    34                 "config": "overlay.netwarnings"
    35             },
    36             {
    37                 "type": "boolean",
    3832                "label": "FPS overlay",
    3933                "tooltip": "Show frames per second in top right corner.",
     
    5751                "tooltip": "Always show the remaining ceasefire time.",
    5852                "config": "gui.session.ceasefirecounter"
    59             },
    60             {
    61                 "type": "dropdown",
    62                 "label": "Late observer joins",
    63                 "tooltip": "Allow everybody or buddies only to join the game as observer after it started.",
    64                 "config": "network.lateobservers",
    65                 "list": [
    66                     { "value": "everyone", "label": "Everyone" },
    67                     { "value": "buddies", "label": "Buddies" },
    68                     { "value": "disabled", "label": "Disabled" }
    69                 ]
    70             },
    71             {
    72                 "type": "number",
    73                 "label": "Observer limit",
    74                 "tooltip": "Prevent further observers from joining if the limit is reached.",
    75                 "config": "network.observerlimit",
    76                 "min": 0,
    77                 "max": 32
    7853            },
    7954            {
     
    426401    },
    427402    {
    428         "label": "Lobby",
     403        "label": "Lobby",
    429404        "tooltip": "These settings only affect the multiplayer.",
    430405        "options":
     
    448423                "tooltip": "Show the average rating of the participating players in a column of the gamelist.",
    449424                "config": "lobby.columns.gamerating"
     425
     426
     427
     428
     429
     430
     431
     432
     433
     434
     435
     436
     437
     438
     439
     440
     441
     442
     443
     444
     445
     446
     447
     448
     449
     450
     451
     452
     453
     454
     455
     456
     457
     458
     459
     460
     461
     462
     463
    450464            }
    451465        ]
  • ps/trunk/binaries/data/mods/public/gui/session/session.js

    r25116 r25156  
    2121var g_MiniMapPanel;
    2222var g_NetworkStatusOverlay;
     23
    2324var g_ObjectivesDialog;
    2425var g_OutOfSyncNetwork;
     
    290291    g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes);
    291292    g_NetworkStatusOverlay = new NetworkStatusOverlay();
     293
    292294    g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl, mapCache);
    293295    g_OutOfSyncNetwork = new OutOfSyncNetwork();
  • ps/trunk/binaries/data/mods/public/gui/session/session.xml

    r24979 r25156  
    3939
    4040    <include file="gui/session/NetworkStatusOverlay.xml"/>
     41
    4142    <include file="gui/session/PauseOverlay.xml"/>
    4243    <include file="gui/session/TimeNotificationOverlay.xml"/>
  • ps/trunk/source/network/NetServer.cpp

    r25099 r25156  
    748748
    749749    if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
    750         m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
     750        m_ServerTurnManager->UninitialiseClient(session->GetHostID());
    751751
    752752    // TODO: ought to switch the player controlled by that client
     
    14031403
    14041404    // Tell the turn manager to expect commands from this new client
    1405     server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
     1405    // Special case: the controller shouldn't be treated as an observer in any case.
     1406    bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID();
     1407    server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver);
    14061408
    14071409    // Tell the client that everything has finished loading and it should start now
     
    15311533
    15321534    for (CNetServerSession* session : m_Sessions)
    1533         m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
     1535    {
     1536        // Special case: the controller shouldn't be treated as an observer in any case.
     1537        bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID();
     1538        m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver);
     1539    }
    15341540
    15351541    m_State = SERVER_STATE_LOADING;
  • ps/trunk/source/network/NetServerTurnManager.cpp

    r25001 r25156  
    2525#include "lib/utf8.h"
    2626#include "ps/CLogger.h"
     27
    2728#include "simulation2/system/TurnManager.h"
    2829
     
    7475void CNetServerTurnManager::CheckClientsReady()
    7576{
     77
     78
     79
     80
     81
    7682    // See if all clients (including self) are ready for a new turn
    7783    for (const std::pair<const int, u32>& clientReady : m_ClientsReady)
    7884    {
     85
     86
     87
    7988        NETSERVERTURN_LOG("  %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn);
    8089        if (clientReady.second <= m_ReadyTurn)
     
    172181}
    173182
    174 void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
     183void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
    175184{
    176185    NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
     
    179188    m_ClientsReady[client] = turn + COMMAND_DELAY_MP - 1;
    180189    m_ClientsSimulated[client] = turn;
     190
    181191}
    182192
     
    188198    m_ClientsReady.erase(client);
    189199    m_ClientsSimulated.erase(client);
     200
    190201
    191202    // Check whether we're ready for the next turn now that we're not
  • ps/trunk/source/network/NetServerTurnManager.h

    r21023 r25156  
    1 /* Copyright (C) 2018 Wildfire Games.
     1/* Copyright (C) 20 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    1919#define INCLUDED_NETSERVERTURNMANAGER
    2020
     21
     22
    2123#include <map>
    22 #include "ps/CStr.h"
     24#include
    2325
    2426class CNetServerWorker;
     
    2830 * The server-side counterpart to CNetClientTurnManager.
    2931 * Records the turn state of each client, and sends turn advancement messages
    30  * when all clients are ready.
     32 * when clients are ready.
    3133 *
    3234 * Thread-safety:
     
    4446
    4547    /**
    46      * Inform the turn manager of a new client who will be sending commands.
     48     * Inform the turn manager of a new client
     49     * @param observer - whether this client is an observer.
    4750     */
    48     void InitialiseClient(int client, u32 turn);
     51    void InitialiseClient(int client, u32 turn);
    4952
    5053    /**
    51      * Inform the turn manager that a previously-initialised client has left the game
    52      * and will no longer be sending commands.
     54     * Inform the turn manager that a previously-initialised client has left the game.
    5355     */
    5456    void UninitialiseClient(int client);
     
    7476    u32 m_ReadyTurn;
    7577
     78
     79
     80
    7681    // Client ID -> ready turn number (the latest turn for which all commands have been received from that client)
    7782    std::map<int, u32> m_ClientsReady;
     
    8590
    8691    // Map of client ID -> playername
    87     std::map<u32, CStrW> m_ClientPlayernames;
     92    std::map<, CStrW> m_ClientPlayernames;
    8893
    8994    // Current turn length
  • ps/trunk/source/ps/scripting/JSInterface_Game.cpp

    r24983 r25156  
    9999}
    100100
     101
     102
     103
     104
     105
     106
     107
     108
     109
     110
     111
    101112bool IsPaused(const ScriptRequest& rq)
    102113{
     
    180191    ScriptFunction::Register<&GetSimRate>(rq, "GetSimRate");
    181192    ScriptFunction::Register<&SetSimRate>(rq, "SetSimRate");
     193
    182194    ScriptFunction::Register<&IsPaused>(rq, "IsPaused");
    183195    ScriptFunction::Register<&SetPaused>(rq, "SetPaused");
  • ps/trunk/source/simulation2/system/TurnManager.cpp

    r25016 r25156  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    3737#endif
    3838
    39 /**
    40  * Maximum number of turns between two clients.
    41  * When we are on turn n, we schedule new commands for n+COMMAND_DELAY.
    42  * We know that all other clients have finished scheduling commands for n,
    43  * else we couldn't have got here, which means they're at least on turn n-COMMAND_DELAY+1.
    44  * We know we have not yet finished scheduling commands for n+COMMAND_DELAY, so no client can be there.
    45  * Hence other clients can be on turns [n-COMMAND_DELAY+1, ..., n+COMMAND_DELAY-1], and no other,
    46  * hence any two clients can only be this many turns apart.
    47  */
    48 constexpr int MaxClientTurnDelta(int commandDelay)
    49 {
    50         return 2 * (commandDelay - 1);
    51 }
    52 
    5339const CStr CTurnManager::EventNameSavegameLoaded = "SavegameLoaded";
    5440
     
    5945    m_QuickSaveMetadata(m_Simulation2.GetScriptInterface().GetGeneralJSContext())
    6046{
    61     // Lag between any two clients is bounded. Add 1 for inclusive bounds.
    62     m_QueuedCommands.resize(MaxClientTurnDelta(m_CommandDelay) + 1);
     47    m_QueuedCommands.resize(1);
    6348}
    6449
     
    238223    NETTURN_LOG("AddCommand(client=%d player=%d turn=%d current=%d, ready=%d)\n", client, player, turn, m_CurrentTurn, m_ReadyTurn);
    239224
    240     // Reject commands for turns that we should not be able to compute (in the past or too far future).
    241     if (m_CurrentTurn >= turn || turn > m_CurrentTurn + MaxClientTurnDelta(m_CommandDelay) + 1)
    242     {
    243         debug_warn(L"Received command for invalid turn");
     225    // Reject commands for turns that we should not be able to compute (in the past).
     226    if (m_CurrentTurn >= turn)
     227    {
     228        // The most likely explanation is that an observer that's lagging behind is sending commands,
     229        // which is possible when cheats are enabled. Report & ignore.
     230        // It seems a bad idea to error out too badly here:
     231        // nefarious clients could try and send broken commands to DOS.
     232        LOGWARNING("Received command for invalid turn %i (current turn is %i)", turn, m_CurrentTurn);
    244233        return;
    245234    }
     
    248237
    249238    ScriptRequest rq(m_Simulation2.GetScriptInterface());
     239
     240
     241
    250242    m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, rq.cx, data);
    251243}
  • ps/trunk/source/simulation2/system/TurnManager.h

    r25001 r25156  
    1 /* Copyright (C) 2020 Wildfire Games.
     1/* Copyright (C) 202 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
     
    156156    void QuickLoad();
    157157
    158     u32 GetCurrentTurn() { return m_CurrentTurn; }
     158    u32 GetCurrentTurn() const { return m_CurrentTurn; }
     159
     160    /**
     161     * @return how many turns are ready to be computed.
     162     * (used to detect players/observers that fall behind the live game.
     163     */
     164    u32 GetPendingTurns() const { return m_ReadyTurn - m_CurrentTurn; }
    159165
    160166protected:
Note: See TracChangeset for help on using the changeset viewer.