diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg
index b49da8636c..b67a3a645e 100644
--- a/binaries/data/config/default.cfg
+++ b/binaries/data/config/default.cfg
@@ -486,6 +486,8 @@ name_id = "0ad"
duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join.
lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled.
observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached
+observermaxlag = 10 ; Make clients wait for observers if they lag more than X turns behind. -1 means "never wait for observers".
+autocatchup = true ; Auto-accelerate the sim rate if lagging behind (as an observer).
[overlay]
fps = "false" ; Show frames per second in top right corner
diff --git a/binaries/data/mods/public/gui/options/options.json b/binaries/data/mods/public/gui/options/options.json
index eccaca9747..ce7c494d46 100644
--- a/binaries/data/mods/public/gui/options/options.json
+++ b/binaries/data/mods/public/gui/options/options.json
@@ -27,12 +27,6 @@
"tooltip": "If you disable it, the welcome screen will still appear once, each time a new version is available. You can always launch it from the main menu.",
"config": "gui.splashscreen.enable"
},
- {
- "type": "boolean",
- "label": "Network warnings",
- "tooltip": "Show which player has a bad connection in multiplayer games.",
- "config": "overlay.netwarnings"
- },
{
"type": "boolean",
"label": "FPS overlay",
@@ -57,25 +51,6 @@
"tooltip": "Always show the remaining ceasefire time.",
"config": "gui.session.ceasefirecounter"
},
- {
- "type": "dropdown",
- "label": "Late observer joins",
- "tooltip": "Allow everybody or buddies only to join the game as observer after it started.",
- "config": "network.lateobservers",
- "list": [
- { "value": "everyone", "label": "Everyone" },
- { "value": "buddies", "label": "Buddies" },
- { "value": "disabled", "label": "Disabled" }
- ]
- },
- {
- "type": "number",
- "label": "Observer limit",
- "tooltip": "Prevent further observers from joining if the limit is reached.",
- "config": "network.observerlimit",
- "min": 0,
- "max": 32
- },
{
"type": "boolean",
"label": "Chat timestamp",
@@ -425,7 +400,7 @@
]
},
{
- "label": "Lobby",
+ "label": "Networking / Lobby",
"tooltip": "These settings only affect the multiplayer.",
"options":
[
@@ -447,6 +422,45 @@
"label": "Game rating column",
"tooltip": "Show the average rating of the participating players in a column of the gamelist.",
"config": "lobby.columns.gamerating"
+ },
+ {
+ "type": "boolean",
+ "label": "Network warnings",
+ "tooltip": "Show which player has a bad connection in multiplayer games.",
+ "config": "overlay.netwarnings"
+ },
+ {
+ "type": "dropdown",
+ "label": "Late observer joins",
+ "tooltip": "Allow everybody or buddies only to join the game as observer after it started.",
+ "config": "network.lateobservers",
+ "list": [
+ { "value": "everyone", "label": "Everyone" },
+ { "value": "buddies", "label": "Buddies" },
+ { "value": "disabled", "label": "Disabled" }
+ ]
+ },
+ {
+ "type": "number",
+ "label": "Observer limit",
+ "tooltip": "Prevent further observers from joining if the limit is reached.",
+ "config": "network.observerlimit",
+ "min": 0,
+ "max": 32
+ },
+ {
+ "type": "number",
+ "label": "Max lag for observers",
+ "tooltip": "When hosting, pause the game if observers are lagging more than this many turns. If set to -1, observers are ignored.",
+ "config": "network.observermaxlag",
+ "min": -1,
+ "max": 10000
+ },
+ {
+ "type": "boolean",
+ "label": "(Observer) Speed up when lagging.",
+ "tooltip": "When observing a game, automatically speed up if you start lagging, to catch up with the live match.",
+ "config": "network.autocatchup"
}
]
},
diff --git a/binaries/data/mods/public/gui/session/NetworkDelayOverlay.js b/binaries/data/mods/public/gui/session/NetworkDelayOverlay.js
new file mode 100644
index 0000000000..a687c8d204
--- /dev/null
+++ b/binaries/data/mods/public/gui/session/NetworkDelayOverlay.js
@@ -0,0 +1,63 @@
+/**
+ * Shows an overlay if the game is lagging behind the net server.
+ */
+class NetworkDelayOverlay
+{
+ constructor()
+ {
+ this.netDelayOverlay = Engine.GetGUIObjectByName("netDelayOverlay");
+
+ this.netDelayOverlay.caption="toto";
+ this.caption = translate(this.Caption);
+ this.sprintfData = {};
+
+ this.initialSimRate = Engine.GetSimRate();
+ this.currentSimRate = this.initialSimRate;
+
+ setTimeout(() => this.CheckDelay(), 1000);
+ }
+
+ CheckDelay()
+ {
+ setTimeout(() => this.CheckDelay(), 1000);
+ let delay = +(Engine.HasNetClient() && Engine.GetPendingTurns());
+
+ if (g_IsObserver && Engine.ConfigDB_GetValue("user", "network.autocatchup"))
+ {
+ if (delay > this.MAX_LIVE_DELAY && this.currentSimRate <= this.initialSimRate)
+ {
+ this.currentSimRate = this.initialSimRate * 1.1;
+ Engine.SetSimRate(this.currentSimRate);
+ }
+ else if (delay <= this.NORMAL_DELAY && this.currentSimRate > this.initialSimRate)
+ {
+ this.currentSimRate = this.initialSimRate;
+ Engine.SetSimRate(this.currentSimRate);
+ }
+ }
+
+ if (delay < this.MAX_LIVE_DELAY)
+ {
+ this.netDelayOverlay.hidden = true;
+ return;
+ }
+ this.netDelayOverlay.hidden = false;
+ this.sprintfData.delay = (delay / this.TURNS_PER_SECOND);
+ this.sprintfData.delay = this.sprintfData.delay.toFixed(this.sprintfData.delay < 5 ? 1 : 0);
+ this.netDelayOverlay.caption = sprintf(this.caption, this.sprintfData);
+ }
+}
+
+/**
+ * Because of command delay, we can still be several turns behind the 'ready' turn and not
+ * particularly late. This should be kept in sync with the command delay.
+ */
+NetworkDelayOverlay.prototype.NORMAL_DELAY = 3;
+NetworkDelayOverlay.prototype.MAX_LIVE_DELAY = 6;
+
+/**
+ * This needs to be kept in sync with the turn length.
+ */
+NetworkDelayOverlay.prototype.TURNS_PER_SECOND = 5;
+
+NetworkDelayOverlay.prototype.Caption = translate("Delay to live stream: %(delay)ss");
diff --git a/binaries/data/mods/public/gui/session/NetworkDelayOverlay.xml b/binaries/data/mods/public/gui/session/NetworkDelayOverlay.xml
new file mode 100644
index 0000000000..7992d99773
--- /dev/null
+++ b/binaries/data/mods/public/gui/session/NetworkDelayOverlay.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js
index 6ef89db5a0..890bcbfc8c 100644
--- a/binaries/data/mods/public/gui/session/session.js
+++ b/binaries/data/mods/public/gui/session/session.js
@@ -20,6 +20,7 @@ var g_GameSpeedControl;
var g_Menu;
var g_MiniMapPanel;
var g_NetworkStatusOverlay;
+var g_NetworkDelayOverlay;
var g_ObjectivesDialog;
var g_OutOfSyncNetwork;
var g_OutOfSyncReplay;
@@ -289,6 +290,7 @@ function init(initData, hotloadData)
g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat);
g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes);
g_NetworkStatusOverlay = new NetworkStatusOverlay();
+ g_NetworkDelayOverlay = new NetworkDelayOverlay();
g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl, mapCache);
g_OutOfSyncNetwork = new OutOfSyncNetwork();
g_OutOfSyncReplay = new OutOfSyncReplay();
diff --git a/binaries/data/mods/public/gui/session/session.xml b/binaries/data/mods/public/gui/session/session.xml
index 723b3c7b4f..4cdaef004b 100644
--- a/binaries/data/mods/public/gui/session/session.xml
+++ b/binaries/data/mods/public/gui/session/session.xml
@@ -38,6 +38,7 @@
+
diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp
index 8f4114a6bf..2674c88219 100644
--- a/source/network/NetServer.cpp
+++ b/source/network/NetServer.cpp
@@ -747,7 +747,7 @@ void CNetServerWorker::OnUserLeave(CNetServerSession* session)
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
- m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
+ m_ServerTurnManager->UninitialiseClient(session->GetHostID());
// TODO: ought to switch the player controlled by that client
// back to AI control, or something?
@@ -1402,7 +1402,9 @@ bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
}
// Tell the turn manager to expect commands from this new client
- server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
+ // Special case: the controller shouldn't be treated as an observer in any case.
+ bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID();
+ server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver);
// Tell the client that everything has finished loading and it should start now
CLoadedGameMessage loaded;
@@ -1530,7 +1532,11 @@ void CNetServerWorker::StartGame(const CStr& initAttribs)
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (CNetServerSession* session : m_Sessions)
- m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
+ {
+ // Special case: the controller shouldn't be treated as an observer in any case.
+ bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID();
+ m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver);
+ }
m_State = SERVER_STATE_LOADING;
diff --git a/source/network/NetServerTurnManager.cpp b/source/network/NetServerTurnManager.cpp
index 567fcd4e43..fa2de8260e 100644
--- a/source/network/NetServerTurnManager.cpp
+++ b/source/network/NetServerTurnManager.cpp
@@ -24,6 +24,7 @@
#include "lib/utf8.h"
#include "ps/CLogger.h"
+#include "ps/ConfigDB.h"
#include "simulation2/system/TurnManager.h"
#if 0
@@ -73,9 +74,17 @@ void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& sess
void CNetServerTurnManager::CheckClientsReady()
{
+ int max_observer_lag = -1;
+ CFG_GET_VAL("network.observermaxlag", max_observer_lag);
+ // Clamp to 0-10000 turns, below/above that is no limit.
+ max_observer_lag = max_observer_lag < 0 ? -1 : max_observer_lag > 10000 ? -1 : max_observer_lag;
+
// See if all clients (including self) are ready for a new turn
for (const std::pair& clientReady : m_ClientsReady)
{
+ // Observers are allowed to lag more than regular clients.
+ if (m_ClientsObserver[clientReady.first] && (max_observer_lag == -1 || clientReady.second > m_ReadyTurn - max_observer_lag))
+ continue;
NETSERVERTURN_LOG(" %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn);
if (clientReady.second <= m_ReadyTurn)
return; // wasn't ready for m_ReadyTurn+1
@@ -171,13 +180,14 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& sessio
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
-void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
+void CNetServerTurnManager::InitialiseClient(int client, u32 turn, bool observer)
{
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end());
m_ClientsReady[client] = turn + COMMAND_DELAY_MP - 1;
m_ClientsSimulated[client] = turn;
+ m_ClientsObserver[client] = observer;
}
void CNetServerTurnManager::UninitialiseClient(int client)
@@ -187,6 +197,7 @@ void CNetServerTurnManager::UninitialiseClient(int client)
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
m_ClientsReady.erase(client);
m_ClientsSimulated.erase(client);
+ m_ClientsObserver.erase(client);
// Check whether we're ready for the next turn now that we're not
// waiting for this client any more
diff --git a/source/network/NetServerTurnManager.h b/source/network/NetServerTurnManager.h
index 2eb3b81b50..1ac71ec7eb 100644
--- a/source/network/NetServerTurnManager.h
+++ b/source/network/NetServerTurnManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -18,16 +18,18 @@
#ifndef INCLUDED_NETSERVERTURNMANAGER
#define INCLUDED_NETSERVERTURNMANAGER
-#include