1
0
forked from 0ad/0ad

MP: don't enforce game init attributes synchronization in PREGAME.

The NetServer stored a complete copy of the game Init Attributes, which
it sent to new clients on updates from the controller. This worked well,
but prevents incremental updates and other unrelated messages from being
sent.

This changes the system so that:
- in PREGAME state, the server does not update its copy of the game init
attributes
- the server forwards game setup messages from the controller to all
clients
- Joining clients get a full copy of the Settings, when joining, from
the controller (this is a js-driven behaviour - other situations might
not need do it).
- Make the StartNetworkGame message take a copy of the final init
attributes, to ensure synchronization (and simplify some logic).

In practice, this:
- makes it possible to send different types of gamesetup messages (this
introduces two: a regular update and the full 'initial-update' for new
clients).
- moves some C++ hardcoding into JS - here in essence the PREGAME server
state is now init-attributes-agnostic.
- does not change much for readiness control - the server already needed
to force a change at game start to set random elements.

Note that the loading page is currently still receiving the 'local' game
attributes, which assumes that all clients are correctly synchronized
(they should be).

Refs #3806, #3049

Differential Revision: https://code.wildfiregames.com/D3714
This was SVN commit r25099.
This commit is contained in:
wraitii 2021-03-22 10:13:27 +00:00
parent 8c2ab4df62
commit 87fc52b780
15 changed files with 192 additions and 133 deletions

View File

@ -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);
}

View File

@ -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()

View File

@ -5,7 +5,7 @@
*/
class GameRegisterStanza
{
constructor(initData, setupWindow, netMessages, gameSettingsControl, mapCache)
constructor(initData, setupWindow, netMessages, mapCache)
{
this.mapCache = mapCache;

View File

@ -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);
}

View File

@ -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;
}
}
};

View File

@ -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.

View File

@ -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
});

View File

@ -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<CNetClient*>(context);
CGameSetupMessage* message = static_cast<CGameSetupMessage*>(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<CNetClient*>(context);
CGameStartMessage* message = static_cast<CGameStartMessage*>(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<CNetClient*>(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<CNetFileReceiveTask>(new CNetFileReceiveTask_ClientRejoin(*client))
shared_ptr<CNetFileReceiveTask>(new CNetFileReceiveTask_ClientRejoin(*client, joinSyncStartMessage->m_InitAttributes))
);
return true;

View File

@ -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;

View File

@ -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)

View File

@ -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<ScriptContext> 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<CStr, CStr>& 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<const CStr, PlayerAssignment>& 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<std::mutex> 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)

View File

@ -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<bool> m_StartGameQueue;
std::vector<std::string> m_GameAttributesQueue;
std::vector<std::string> m_InitAttributesQueue;
std::vector<std::pair<CStr, CStr>> m_LobbyAuthQueue;
std::vector<u32> m_TurnLengthQueue;
};

View File

@ -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");

View File

@ -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);

View File

@ -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);