diff --git a/binaries/data/mods/public/gui/gamesettings/GameSettings.js b/binaries/data/mods/public/gui/gamesettings/GameSettings.js index 2d3572dc90..5d4ba8a30e 100644 --- a/binaries/data/mods/public/gui/gamesettings/GameSettings.js +++ b/binaries/data/mods/public/gui/gamesettings/GameSettings.js @@ -28,10 +28,6 @@ class GameSettings "value": Engine.HasNetClient(), }); - Object.defineProperty(this, "isController", { - "value": !this.isNetworked || Engine.IsNetController(), - }); - // Load attributes as regular enumerable (i.e. iterable) properties. for (let comp in GameSettings.prototype.Attributes) { @@ -86,15 +82,6 @@ class GameSettings this[comp].fromInitAttributes(attribs); } - /** - * Send the game settings to the server. - */ - setNetworkInitAttributes() - { - if (this.isNetworked && this.isController) - Engine.SetNetworkInitAttributes(this.toInitAttributes()); - } - /** * Change "random" settings into their proper settings. */ @@ -128,7 +115,6 @@ class GameSettings this.pickRandomItems(); Engine.SetRankedGame(this.rating.enabled); - this.setNetworkInitAttributes(); // Replace player names with the real players. for (let guid in playerAssignments) @@ -137,7 +123,7 @@ class GameSettings // NB: for multiplayer support, the clients must be listening to "start" net messages. if (this.isNetworked) - Engine.StartNetworkGame(); + Engine.StartNetworkGame(this.toInitAttributes()); else Engine.StartGame(this.toInitAttributes(), playerAssignments.local.player); } diff --git a/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js b/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js index ea8a0cf7c5..f6052a414c 100644 --- a/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js +++ b/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js @@ -3,16 +3,22 @@ */ class GameSettingsControl { - constructor(setupWindow, netMessages, startGameControl, mapCache) + constructor(setupWindow, netMessages, startGameControl, playerAssignmentsControl, mapCache) { + this.setupWindow = setupWindow; this.startGameControl = startGameControl; this.mapCache = mapCache; this.gameSettingsFile = new GameSettingsFile(this); this.guiData = new GameSettingsGuiData(); + // When joining a game, the complete set of attributes + // may not have been received yet. + this.loading = true; + this.updateLayoutHandlers = new Set(); this.settingsChangeHandlers = new Set(); + this.loadingChangeHandlers = new Set(); setupWindow.registerLoadHandler(this.onLoad.bind(this)); setupWindow.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this)); @@ -21,10 +27,16 @@ class GameSettingsControl setupWindow.registerClosePageHandler(this.onClose.bind(this)); + if (g_IsController && g_IsNetworked) + playerAssignmentsControl.registerClientJoinHandler(this.onClientJoin.bind(this)); + if (g_IsNetworked) netMessages.registerNetMessageHandler("gamesetup", this.onGamesetupMessage.bind(this)); } + /** + * @param handler will be called when the layout needs to be updated. + */ registerUpdateLayoutHandler(handler) { this.updateLayoutHandlers.add(handler); @@ -39,6 +51,14 @@ class GameSettingsControl this.settingsChangeHandlers.add(handler); } + /** + * @param handler will be called when the 'loading' state change. + */ + registerLoadingChangeHandler(handler) + { + this.loadingChangeHandlers.add(handler); + } + onLoad(initData, hotloadData) { if (hotloadData) @@ -52,6 +72,10 @@ class GameSettingsControl this.updateLayout(); this.setNetworkInitAttributes(); + + // If we are the controller, we are done loading. + if (hotloadData || !g_IsNetworked || g_IsController) + this.setLoading(false); } onClose() @@ -59,6 +83,37 @@ class GameSettingsControl this.gameSettingsFile.saveFile(); } + onClientJoin() + { + /** + * A note on network synchronization: + * The net server does not keep the current state of attributes, + * nor does it act like a message queue, so a new client + * will only receive updates after they've joined. + * In particular, new joiners start with no information, + * so the controller must first send them a complete copy of the settings. + * However, messages could be in-flight towards the controller, + * but the new client may never receive these or have already received them, + * leading to an ordering issue that might desync the new client. + * + * The simplest solution is to have the (single) controller + * act as the single source of truth. Any other message must + * first go through the controller, which will send updates. + * This enforces the ordering of the controller. + * In practical terms, if e.g. players controlling their own civ is implemented, + * the message will need to be ignored by everyone but the controller, + * and the controller will need to send an update once it rejects/accepts the changes, + * which will then update the other clients. + * Of course, the original client GUI may want to temporarily show a different state. + * Note that the final attributes are sent on game start anyways, so any + * synchronization issue that might happen at that point can be resolved. + */ + Engine.SendGameSetupMessage({ + "type": "initial-update", + "initAttribs": this.getSettings() + }); + } + onGetHotloadData(object) { object.initAttributes = this.getSettings(); @@ -66,10 +121,26 @@ class GameSettingsControl onGamesetupMessage(message) { + // For now, the controller only can send updates, so no need to listen to messages. if (!message.data || g_IsController) return; - this.parseSettings(message.data); + if (message.data.type !== "update" && + message.data.type !== "initial-update") + { + error("Unknown message type " + message.data.type); + return; + } + + if (message.data.type === "initial-update") + { + // Ignore initial updates if we've already received settings. + if (!this.loading) + return; + this.setLoading(false); + } + + this.parseSettings(message.data.initAttribs); // This assumes that messages aren't sent spuriously without changes // (which is generally fair), but technically it would be good @@ -98,6 +169,15 @@ class GameSettingsControl g_GameSettings.fromInitAttributes(settings); } + setLoading(loading) + { + if (this.loading === loading) + return; + this.loading = loading; + for (let handler of this.loadingChangeHandlers) + handler(); + } + /** * This should be called whenever the GUI layout needs to be updated. * Triggers on the next GUI tick to avoid un-necessary layout. @@ -138,7 +218,12 @@ class GameSettingsControl clearTimeout(this.timer); delete this.timer; } - g_GameSettings.setNetworkInitAttributes(); + // See note in onClientJoin on network synchronization. + if (g_IsController) + Engine.SendGameSetupMessage({ + "type": "update", + "initAttribs": this.getSettings() + }); } onLaunchGame() diff --git a/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js b/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js index 0e8be21c79..136a48f34e 100644 --- a/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js +++ b/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js @@ -5,7 +5,7 @@ */ class GameRegisterStanza { - constructor(initData, setupWindow, netMessages, gameSettingsControl, mapCache) + constructor(initData, setupWindow, netMessages, mapCache) { this.mapCache = mapCache; diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/MapFilter.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/MapFilter.js index 25b8a82d7e..1712bc5fe6 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/MapFilter.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/MapFilter.js @@ -6,6 +6,7 @@ GameSettingControls.MapFilter = class MapFilter extends GameSettingControlDropdo this.values = undefined; + this.gameSettingsControl.guiData.mapFilter.watch(() => this.render(), ["filter"]); g_GameSettings.map.watch(() => this.checkMapTypeChange(), ["type"]); } @@ -37,9 +38,13 @@ GameSettingControls.MapFilter = class MapFilter extends GameSettingControlDropdo this.gameSettingsControl.guiData.mapFilter.filter = this.values.Name[this.values.Default]; this.gameSettingsControl.setNetworkInitAttributes(); } + this.render(); + } + + render() + { // Index may have changed, reset. this.setSelectedValue(this.gameSettingsControl.guiData.mapFilter.filter); - this.setHidden(!this.values); } diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/LoadingPage/LoadingPage.js b/binaries/data/mods/public/gui/gamesetup/Pages/LoadingPage/LoadingPage.js index 173373d54a..f28744fb23 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/LoadingPage/LoadingPage.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/LoadingPage/LoadingPage.js @@ -7,19 +7,16 @@ SetupWindowPages.LoadingPage = class { constructor(setupWindow) { - if (g_IsNetworked) - setupWindow.controls.netMessages.registerNetMessageHandler("gamesetup", this.hideLoadingPage.bind(this)); - else - this.hideLoadingPage(); + setupWindow.controls.gameSettingsControl.registerLoadingChangeHandler((loading) => this.onLoadingChange(loading)); } - hideLoadingPage() + onLoadingChange(loading) { let loadingPage = Engine.GetGUIObjectByName("loadingPage"); - if (loadingPage.hidden) + if (loadingPage.hidden === !loading) return; - loadingPage.hidden = true; - Engine.GetGUIObjectByName("setupWindow").hidden = false; + loadingPage.hidden = !loading; + Engine.GetGUIObjectByName("setupWindow").hidden = loading; } -} +}; diff --git a/binaries/data/mods/public/gui/gamesetup/SetupWindow.js b/binaries/data/mods/public/gui/gamesetup/SetupWindow.js index a0070463a5..838b7df4ce 100644 --- a/binaries/data/mods/public/gui/gamesetup/SetupWindow.js +++ b/binaries/data/mods/public/gui/gamesetup/SetupWindow.js @@ -29,10 +29,10 @@ class SetupWindow let netMessages = new NetMessages(this); let startGameControl = new StartGameControl(netMessages); let mapFilters = new MapFilters(mapCache); - let gameSettingsControl = new GameSettingsControl(this, netMessages, startGameControl, mapCache); let gameRegisterStanza = Engine.HasXmppClient() && - new GameRegisterStanza(initData, this, netMessages, gameSettingsControl, mapCache); + new GameRegisterStanza(initData, this, netMessages, mapCache); let playerAssignmentsControl = new PlayerAssignmentsControl(this, netMessages, gameRegisterStanza); + let gameSettingsControl = new GameSettingsControl(this, netMessages, startGameControl, playerAssignmentsControl, mapCache); let readyControl = new ReadyControl(netMessages, gameSettingsControl, startGameControl, playerAssignmentsControl); // These class instances control central data and do not manage any GUI Object. diff --git a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js index b6b0e31a98..f269191c2b 100644 --- a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js @@ -21,7 +21,6 @@ var g_ServerHasPassword = false; var g_ServerId; var g_IsRejoining = false; -var g_InitAttributes; // used when rejoining var g_PlayerAssignments; // used when rejoining var g_UserRating; @@ -226,25 +225,13 @@ function pollAndHandleNetworkClient() } break; - case "gamesetup": - g_InitAttributes = message.data; - break; - case "players": g_PlayerAssignments = message.newAssignments; break; case "start": - // Copy playernames from initial player assignment to the settings - for (let guid in g_PlayerAssignments) - { - let player = g_PlayerAssignments[guid]; - if (player.player > 0) // not observer or GAIA - g_InitAttributes.settings.PlayerData[player.player - 1].Name = player.name; - } - Engine.SwitchGuiPage("page_loading.xml", { - "attribs": g_InitAttributes, + "attribs": message.initAttributes, "isRejoining": g_IsRejoining, "playerAssignments": g_PlayerAssignments }); diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index de263f986f..3e4e6d7500 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -58,8 +58,8 @@ class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ClientRejoin); public: - CNetFileReceiveTask_ClientRejoin(CNetClient& client) - : m_Client(client) + CNetFileReceiveTask_ClientRejoin(CNetClient& client, const CStr& initAttribs) + : m_Client(client), m_InitAttributes(initAttribs) { } @@ -72,18 +72,19 @@ public: // Pretend the server told us to start the game CGameStartMessage start; + start.m_InitAttributes = m_InitAttributes; m_Client.HandleMessage(&start); } private: CNetClient& m_Client; + CStr m_InitAttributes; }; CNetClient::CNetClient(CGame* game) : m_Session(NULL), m_UserName(L"anonymous"), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game), - m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetGeneralJSContext()), m_LastConnectionCheck(0), m_ServerAddress(), m_ServerPort(0), @@ -103,9 +104,7 @@ CNetClient::CNetClient(CGame* game) : AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context); AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, (void*)&OnAuthenticateRequest, context); - AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context); - - AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); + AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_PREGAME, (void*)&OnAuthenticate, context); AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context); AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context); @@ -474,9 +473,10 @@ void CNetClient::SendClearAllReadyMessage() SendMessage(&clearAllReady); } -void CNetClient::SendStartGameMessage() +void CNetClient::SendStartGameMessage(const CStr& initAttribs) { CGameStartMessage gameStart; + gameStart.m_InitAttributes = initAttribs; SendMessage(&gameStart); } @@ -717,8 +717,6 @@ bool CNetClient::OnGameSetup(void* context, CFsmEvent* event) CNetClient* client = static_cast(context); CGameSetupMessage* message = static_cast(event->GetParamRef()); - client->m_GameAttributes = message->m_Data; - client->PushGuiMessage( "type", "gamesetup", "data", message->m_Data); @@ -759,6 +757,7 @@ bool CNetClient::OnGameStart(void* context, CFsmEvent* event) ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetClient* client = static_cast(context); + CGameStartMessage* message = static_cast(event->GetParamRef()); // Find the player assigned to our GUID int player = -1; @@ -768,10 +767,17 @@ bool CNetClient::OnGameStart(void* context, CFsmEvent* event) client->m_ClientTurnManager = new CNetClientTurnManager( *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); - client->m_Game->SetPlayerID(player); - client->m_Game->StartGame(&client->m_GameAttributes, ""); + // Parse init attributes. + const ScriptInterface& scriptInterface = client->m_Game->GetSimulation2()->GetScriptInterface(); + ScriptRequest rq(scriptInterface); + JS::RootedValue initAttribs(rq.cx); + scriptInterface.ParseJSON(message->m_InitAttributes, &initAttribs); - client->PushGuiMessage("type", "start"); + client->m_Game->SetPlayerID(player); + client->m_Game->StartGame(&initAttribs, ""); + + client->PushGuiMessage("type", "start", + "initAttributes", initAttribs); return true; } @@ -782,9 +788,11 @@ bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event) CNetClient* client = static_cast(context); + CJoinSyncStartMessage* joinSyncStartMessage = (CJoinSyncStartMessage*)event->GetParamRef(); + // The server wants us to start downloading the game state from it, so do so client->m_Session->GetFileTransferer().StartTask( - shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client)) + shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client, joinSyncStartMessage->m_InitAttributes)) ); return true; diff --git a/source/network/NetClient.h b/source/network/NetClient.h index dbd36dd0e3..a8f7b8e86c 100644 --- a/source/network/NetClient.h +++ b/source/network/NetClient.h @@ -44,7 +44,6 @@ enum NCS_CONNECT, NCS_HANDSHAKE, NCS_AUTHENTICATE, - NCS_INITIAL_GAMESETUP, NCS_PREGAME, NCS_LOADING, NCS_JOIN_SYNCING, @@ -231,7 +230,7 @@ public: void SendClearAllReadyMessage(); - void SendStartGameMessage(); + void SendStartGameMessage(const CStr& initAttribs); /** * Call when the client has rejoined a running match and finished @@ -326,9 +325,6 @@ private: /// True if the player is currently rejoining or has already rejoined the game. bool m_Rejoin; - /// Latest copy of game setup attributes heard from the server - JS::PersistentRootedValue m_GameAttributes; - /// Latest copy of player assignments heard from the server PlayerAssignmentMap m_PlayerAssignments; diff --git a/source/network/NetMessages.h b/source/network/NetMessages.h index ff65319547..f2259d0a7e 100644 --- a/source/network/NetMessages.h +++ b/source/network/NetMessages.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 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 @@ -28,7 +28,7 @@ #define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?' #define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!' -#define PS_PROTOCOL_VERSION 0x01010017 // Arbitrary protocol +#define PS_PROTOCOL_VERSION 0x01010018 // Arbitrary protocol #define PS_DEFAULT_PORT 0x5073 // 'P', 's' // Set when lobby authentication is required. Used in the SrvHandshakeResponseMessage. @@ -174,6 +174,7 @@ START_NMT_CLASS_(FileTransferAck, NMT_FILE_TRANSFER_ACK) END_NMT_CLASS() START_NMT_CLASS_(JoinSyncStart, NMT_JOIN_SYNC_START) + NMT_FIELD(CStr, m_InitAttributes) END_NMT_CLASS() START_NMT_CLASS_(Rejoined, NMT_REJOINED) @@ -213,6 +214,7 @@ START_NMT_CLASS_(LoadedGame, NMT_LOADED_GAME) END_NMT_CLASS() START_NMT_CLASS_(GameStart, NMT_GAME_START) + NMT_FIELD(CStr, m_InitAttributes) END_NMT_CLASS() START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH) diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index d6546990d1..8f4114a6bf 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -126,7 +126,10 @@ public: // they'll race and get whichever happens to be the latest received by the server, // which should still work but isn't great m_Server.m_JoinSyncFile = m_Buffer; + + // Send the init attributes alongside - these should be correct since the game should be started. CJoinSyncStartMessage message; + message.m_InitAttributes = m_Server.GetScriptInterface().StringifyJSON(&m_Server.m_InitAttributes); session->SendMessage(&message); } @@ -411,7 +414,7 @@ void CNetServerWorker::Run() // We create a new ScriptContext for this network thread, with a single ScriptInterface. shared_ptr netServerContext = ScriptContext::CreateContext(); m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext); - m_GameAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue()); + m_InitAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue()); while (true) { @@ -420,7 +423,7 @@ void CNetServerWorker::Run() // Implement autostart mode if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers) - StartGame(); + StartGame(m_ScriptInterface->StringifyJSON(&m_InitAttributes)); // Update profiler stats m_Stats->LatchHostState(m_Host); @@ -454,25 +457,26 @@ bool CNetServerWorker::RunStep() return false; newStartGame.swap(m_StartGameQueue); - newGameAttributes.swap(m_GameAttributesQueue); + newGameAttributes.swap(m_InitAttributesQueue); newLobbyAuths.swap(m_LobbyAuthQueue); newTurnLength.swap(m_TurnLengthQueue); } if (!newGameAttributes.empty()) { - JS::RootedValue gameAttributesVal(rq.cx); - GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal); - UpdateGameAttributes(&gameAttributesVal); + if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME) + LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading."); + else + { + JS::RootedValue gameAttributesVal(rq.cx); + GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal); + m_InitAttributes = gameAttributesVal; + } } if (!newTurnLength.empty()) SetTurnLength(newTurnLength.back()); - // Do StartGame last, so we have the most up-to-date game attributes when we start - if (!newStartGame.empty()) - StartGame(); - while (!newLobbyAuths.empty()) { const std::pair& auth = newLobbyAuths.back(); @@ -690,7 +694,7 @@ void CNetServerWorker::SetupSession(CNetServerSession* session) session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context); session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context); - session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnStartGame, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnGameStart, context); session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context); @@ -729,10 +733,6 @@ void CNetServerWorker::OnUserJoin(CNetServerSession* session) { AddPlayer(session->GetGUID(), session->GetUserName()); - CGameSetupMessage gameSetupMessage(GetScriptInterface()); - gameSetupMessage.m_Data = m_GameAttributes; - session->SendMessage(&gameSetupMessage); - CPlayerAssignmentMessage assignMessage; ConstructPlayerAssignmentMessage(assignMessage); session->SendMessage(&assignMessage); @@ -1145,6 +1145,8 @@ bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event) if (isRejoining) { + ENSURE(server.m_State != SERVER_STATE_UNCONNECTED && server.m_State != SERVER_STATE_PREGAME); + // Request a copy of the current game state from an existing player, // so we can send it on to the new player @@ -1176,7 +1178,7 @@ bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event) const ScriptInterface& scriptInterface = server.GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue settings(rq.cx); - scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings); + scriptInterface.GetProperty(server.m_InitAttributes, "settings", &settings); if (scriptInterface.HasProperty(settings, "CheatsEnabled")) scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled); @@ -1288,10 +1290,14 @@ bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event) if (server.m_State != SERVER_STATE_PREGAME) return true; + // Only the controller is allowed to send game setup updates. + // TODO: it would be good to allow other players to request changes to some settings, + // e.g. their civilisation. + // Possibly this should use another message, to enforce a single source of truth. if (session->GetGUID() == server.m_ControllerGUID) { CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); - server.UpdateGameAttributes(&(message->m_Data)); + server.Broadcast(message, { NSS_PREGAME }); } return true; } @@ -1310,15 +1316,17 @@ bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event) return true; } -bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event) +bool CNetServerWorker::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); - if (session->GetGUID() == server.m_ControllerGUID) - server.StartGame(); + if (session->GetGUID() != server.m_ControllerGUID) + return true; + CGameStartMessage* message = (CGameStartMessage*)event->GetParamRef(); + server.StartGame(message->m_InitAttributes); return true; } @@ -1510,7 +1518,7 @@ bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession) return true; } -void CNetServerWorker::StartGame() +void CNetServerWorker::StartGame(const CStr& initAttribs) { for (std::pair& player : m_PlayerAssignments) if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0) @@ -1526,9 +1534,6 @@ void CNetServerWorker::StartGame() m_State = SERVER_STATE_LOADING; - // Send the final setup state to all clients - UpdateGameAttributes(&m_GameAttributes); - // Remove players and observers that are not present when the game starts for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();) if (it->second.m_Enabled) @@ -1538,22 +1543,14 @@ void CNetServerWorker::StartGame() SendPlayerAssignments(); + // Update init attributes. They should no longer change. + m_ScriptInterface->ParseJSON(initAttribs, &m_InitAttributes); + CGameStartMessage gameStart; + gameStart.m_InitAttributes = initAttribs; Broadcast(&gameStart, { NSS_PREGAME }); } -void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs) -{ - m_GameAttributes = attrs; - - if (!m_Host) - return; - - CGameSetupMessage gameSetupMessage(GetScriptInterface()); - gameSetupMessage.m_Data = m_GameAttributes; - Broadcast(&gameSetupMessage, { NSS_PREGAME }); -} - CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) { const size_t MAX_LENGTH = 32; @@ -1694,14 +1691,14 @@ void CNetServer::StartGame() m_Worker->m_StartGameQueue.push_back(true); } -void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface) +void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface) { // Pass the attributes as JSON, since that's the easiest safe // cross-thread way of passing script data std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false); std::lock_guard lock(m_Worker->m_WorkerMutex); - m_Worker->m_GameAttributesQueue.push_back(attrsJSON); + m_Worker->m_InitAttributesQueue.push_back(attrsJSON); } void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token) diff --git a/source/network/NetServer.h b/source/network/NetServer.h index f2045fc463..cc677b6fe0 100644 --- a/source/network/NetServer.h +++ b/source/network/NetServer.h @@ -125,16 +125,16 @@ public: /** * Call from the GUI to asynchronously notify all clients that they should start loading the game. + * UpdateInitAttributes must be called at least once. */ void StartGame(); /** * Call from the GUI to update the game setup attributes. - * This must be called at least once before starting the game. - * The changes will be asynchronously propagated to all clients. - * @param attrs game attributes, in the script context of scriptInterface + * The changes won't be propagated to clients until game start. + * @param attrs init attributes, in the script context of scriptInterface */ - void UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface); + void UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface); /** * Set the turn length to a fixed value. @@ -237,25 +237,15 @@ private: bool SetupConnection(const u16 port); /** - * Call from the GUI to update the player assignments. * The given GUID will be (re)assigned to the given player ID. * Any player currently using that ID will be unassigned. - * The changes will be propagated to all clients. */ void AssignPlayer(int playerID, const CStr& guid); /** - * Call from the GUI to notify all clients that they should start loading the game. + * Switch in game mode and notify all clients to start the game. */ - void StartGame(); - - /** - * Call from the GUI to update the game setup attributes. - * This must be called at least once before starting the game. - * The changes will be propagated to all clients. - * @param attrs game attributes, in the script context of GetScriptInterface() - */ - void UpdateGameAttributes(JS::MutableHandleValue attrs); + void StartGame(const CStr& initAttribs); /** * Make a player name 'nicer' by limiting the length and removing forbidden characters etc. @@ -268,7 +258,7 @@ private: CStrW DeduplicatePlayerName(const CStrW& original); /** - * Get the script context used for game attributes. + * Get the script context used for init attributes. */ const ScriptInterface& GetScriptInterface(); @@ -301,7 +291,7 @@ private: static bool OnClearAllReady(void* context, CFsmEvent* event); static bool OnGameSetup(void* context, CFsmEvent* event); static bool OnAssignPlayer(void* context, CFsmEvent* event); - static bool OnStartGame(void* context, CFsmEvent* event); + static bool OnGameStart(void* context, CFsmEvent* event); static bool OnLoadedGame(void* context, CFsmEvent* event); static bool OnJoinSyncingLoadedGame(void* context, CFsmEvent* event); static bool OnRejoined(void* context, CFsmEvent* event); @@ -330,7 +320,7 @@ private: /** * Internal script context for (de)serializing script messages, - * and for storing game attributes. + * and for storing init attributes. * (TODO: we shouldn't bother deserializing (except for debug printing of messages), * we should just forward messages blindly and efficiently.) */ @@ -339,9 +329,11 @@ private: PlayerAssignmentMap m_PlayerAssignments; /** - * Stores the most current game attributes. + * Stores the most current init attributes. + * NB: this is not guaranteed to be up-to-date until the server is LOADING or INGAME. + * At that point, the settings are frozen and ought to be identical to the simulation Init Attributes. */ - JS::PersistentRootedValue m_GameAttributes; + JS::PersistentRootedValue m_InitAttributes; int m_AutostartPlayers; @@ -424,7 +416,7 @@ private: // Queues for messages sent by the game thread (protected by m_WorkerMutex): std::vector m_StartGameQueue; - std::vector m_GameAttributesQueue; + std::vector m_InitAttributesQueue; std::vector> m_LobbyAuthQueue; std::vector m_TurnLengthQueue; }; diff --git a/source/network/scripting/JSInterface_Network.cpp b/source/network/scripting/JSInterface_Network.cpp index e4d9493a5b..b845609f79 100644 --- a/source/network/scripting/JSInterface_Network.cpp +++ b/source/network/scripting/JSInterface_Network.cpp @@ -221,7 +221,7 @@ JS::Value PollNetworkClient(const ScriptInterface& scriptInterface) return scriptInterface.CloneValueFromOtherCompartment(g_NetClient->GetScriptInterface(), pollNet); } -void SetNetworkInitAttributes(const ScriptInterface& scriptInterface, JS::HandleValue attribs1) +void SendGameSetupMessage(const ScriptInterface& scriptInterface, JS::HandleValue attribs1) { ENSURE(g_NetClient); @@ -267,10 +267,14 @@ void ClearAllPlayerReady () g_NetClient->SendClearAllReadyMessage(); } -void StartNetworkGame() +void StartNetworkGame(const ScriptInterface& scriptInterface, JS::HandleValue attribs1) { ENSURE(g_NetClient); - g_NetClient->SendStartGameMessage(); + + // TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason). + ScriptRequest rq(scriptInterface); + JS::RootedValue attribs(rq.cx, attribs1); + g_NetClient->SendStartGameMessage(scriptInterface.StringifyJSON(&attribs)); } void SetTurnLength(int length) @@ -293,7 +297,7 @@ void RegisterScriptFunctions(const ScriptRequest& rq) ScriptFunction::Register<&DisconnectNetworkGame>(rq, "DisconnectNetworkGame"); ScriptFunction::Register<&GetPlayerGUID>(rq, "GetPlayerGUID"); ScriptFunction::Register<&PollNetworkClient>(rq, "PollNetworkClient"); - ScriptFunction::Register<&SetNetworkInitAttributes>(rq, "SetNetworkInitAttributes"); + ScriptFunction::Register<&SendGameSetupMessage>(rq, "SendGameSetupMessage"); ScriptFunction::Register<&AssignNetworkPlayer>(rq, "AssignNetworkPlayer"); ScriptFunction::Register<&KickPlayer>(rq, "KickPlayer"); ScriptFunction::Register<&SendNetworkChat>(rq, "SendNetworkChat"); diff --git a/source/network/tests/test_Net.h b/source/network/tests/test_Net.h index 7d1e593289..fb87a5e6af 100644 --- a/source/network/tests/test_Net.h +++ b/source/network/tests/test_Net.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 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 @@ -161,7 +161,7 @@ public: "mapPath", "maps/scenarios/", "thing", "example"); - server.UpdateGameAttributes(&attrs, scriptInterface); + server.UpdateInitAttributes(&attrs, scriptInterface); CNetClient client1(&client1Game); CNetClient client2(&client2Game); @@ -240,7 +240,7 @@ public: "mapPath", "maps/scenarios/", "thing", "example"); - server.UpdateGameAttributes(&attrs, scriptInterface); + server.UpdateInitAttributes(&attrs, scriptInterface); CNetClient client1(&client1Game); CNetClient client2(&client2Game); diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 0997d4f2b9..c79367a7da 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -1563,7 +1563,7 @@ bool Autostart(const CmdLineArgs& args) g_NetServer = new CNetServer(false, maxPlayers); g_NetServer->SetControllerSecret(secret); - g_NetServer->UpdateGameAttributes(&attrs, scriptInterface); + g_NetServer->UpdateInitAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT); ENSURE(ok);