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:
parent
8c2ab4df62
commit
87fc52b780
@ -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);
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
class GameRegisterStanza
|
||||
{
|
||||
constructor(initData, setupWindow, netMessages, gameSettingsControl, mapCache)
|
||||
constructor(initData, setupWindow, netMessages, mapCache)
|
||||
{
|
||||
this.mapCache = mapCache;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user