Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/PlayerAssignmentsControl.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/PlayerAssignmentsControl.js (revision 24685) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/PlayerAssignmentsControl.js (revision 24686) @@ -1,164 +1,168 @@ /** * This class provides a property independent interface to g_PlayerAssignment events and actions. */ class PlayerAssignmentsControl { - constructor(setupWindow, netMessages) + constructor(setupWindow, netMessages, gameRegisterStanza) { this.clientJoinHandlers = new Set(); this.clientLeaveHandlers = new Set(); this.playerAssignmentsChangeHandlers = new Set(); + this.gameRegisterStanza = gameRegisterStanza; if (!g_IsNetworked) { let name = singleplayerName(); // Replace empty player name when entering a single-player match for the first time. Engine.ConfigDB_CreateAndWriteValueToFile("user", this.ConfigNameSingleplayer, name, "config/user.cfg"); g_PlayerAssignments = { "local": { "name": name, "player": -1 } }; } setupWindow.registerLoadHandler(this.onLoad.bind(this)); setupWindow.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this)); netMessages.registerNetMessageHandler("players", this.onPlayerAssignmentMessage.bind(this)); } registerPlayerAssignmentsChangeHandler(handler) { this.playerAssignmentsChangeHandlers.add(handler); } unregisterPlayerAssignmentsChangeHandler(handler) { this.playerAssignmentsChangeHandlers.delete(handler); } registerClientJoinHandler(handler) { this.clientJoinHandlers.add(handler); } unregisterClientJoinHandler(handler) { this.clientJoinHandlers.delete(handler); } registerClientLeaveHandler(handler) { this.clientLeaveHandlers.add(handler); } unregisterClientLeaveHandler(handler) { this.clientLeaveHandlers.delete(handler); } onLoad(initData, hotloadData) { if (hotloadData) { g_PlayerAssignments = hotloadData.playerAssignments; this.updatePlayerAssignments(); } } onGetHotloadData(object) { object.playerAssignments = g_PlayerAssignments; } /** * To be called when g_PlayerAssignments is modified. */ updatePlayerAssignments() { Engine.ProfileStart("updatePlayerAssignments"); for (let handler of this.playerAssignmentsChangeHandlers) handler(); Engine.ProfileStop(); } /** * Called whenever a client joins or leaves or any game setting is changed. */ onPlayerAssignmentMessage(message) { let newAssignments = message.newAssignments; for (let guid in newAssignments) if (!g_PlayerAssignments[guid]) for (let handler of this.clientJoinHandlers) handler(guid, message.newAssignments); for (let guid in g_PlayerAssignments) if (!newAssignments[guid]) for (let handler of this.clientLeaveHandlers) handler(guid); g_PlayerAssignments = newAssignments; this.updatePlayerAssignments(); + // Send at most one gameRegisterStanza after all handlers run in case a + // joining observer has been assigned to a playerslot. + this.gameRegisterStanza.sendImmediately?.(); } assignClient(guid, playerIndex) { if (g_IsNetworked) Engine.AssignNetworkPlayer(playerIndex, guid); else { g_PlayerAssignments[guid].player = playerIndex; this.updatePlayerAssignments(); } } /** * If both clients are assigned players, this will swap their assignments. */ assignPlayer(guidToAssign, playerIndex) { if (g_PlayerAssignments[guidToAssign].player != -1) for (let guid in g_PlayerAssignments) if (g_PlayerAssignments[guid].player == playerIndex + 1) { this.assignClient(guid, g_PlayerAssignments[guidToAssign].player); break; } this.assignClient(guidToAssign, playerIndex + 1); if (!g_IsNetworked) this.updatePlayerAssignments(); } unassignClient(playerID) { if (g_IsNetworked) Engine.AssignNetworkPlayer(playerID, ""); else if (g_PlayerAssignments.local.player == playerID) { g_PlayerAssignments.local.player = -1; this.updatePlayerAssignments(); } } unassignInvalidPlayers() { if (g_IsNetworked) for (let playerID = g_GameAttributes.settings.PlayerData.length + 1; playerID <= g_MaxPlayers; ++playerID) // Remove obsolete playerIDs from the servers playerassignments copy Engine.AssignNetworkPlayer(playerID, ""); else if (g_PlayerAssignments.local.player > g_GameAttributes.settings.PlayerData.length) { g_PlayerAssignments.local.player = -1; this.updatePlayerAssignments(); } } } PlayerAssignmentsControl.prototype.ConfigNameSingleplayer = "playername.singleplayer"; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js (revision 24685) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js (revision 24686) @@ -1,146 +1,142 @@ /** * If there is an XmppClient, this class informs the XPartaMuPP lobby bot that * this match is being setup so that others can join. * It informs of the lobby of some setting values and the participating clients. */ class GameRegisterStanza { - constructor(initData, setupWindow, netMessages, gameSettingsControl, playerAssignmentsControl, mapCache) + constructor(initData, setupWindow, netMessages, gameSettingsControl, mapCache) { this.mapCache = mapCache; this.serverName = initData.serverName; this.serverPort = initData.serverPort; this.stunEndpoint = initData.stunEndpoint; this.mods = JSON.stringify(Engine.GetEngineInfo().mods); this.timer = undefined; // Only send a lobby update when its data changed this.lastStanza = undefined; // Events - let sendImmediately = this.sendImmediately.bind(this); - playerAssignmentsControl.registerClientJoinHandler(sendImmediately); - playerAssignmentsControl.registerClientLeaveHandler(sendImmediately); - setupWindow.registerClosePageHandler(this.onClosePage.bind(this)); gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this)); netMessages.registerNetMessageHandler("start", this.onGameStart.bind(this)); } onGameAttributesBatchChange() { if (this.lastStanza) this.sendDelayed(); else this.sendImmediately(); } onGameStart() { if (!g_IsController || !Engine.HasXmppClient()) return; this.sendImmediately(); let clients = this.formatClientsForStanza(); Engine.SendChangeStateGame(clients.connectedPlayers, clients.list); } onClosePage() { if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); } /** * Send the relevant game settings to the lobby bot in a deferred manner. */ sendDelayed() { if (!g_IsController || !Engine.HasXmppClient()) return; if (this.timer !== undefined) clearTimeout(this.timer); this.timer = setTimeout(this.sendImmediately.bind(this), this.Timeout); } /** * Send the relevant game settings to the lobby bot immediately. */ sendImmediately() { if (!g_IsController || !Engine.HasXmppClient()) return; Engine.ProfileStart("sendRegisterGameStanza"); if (this.timer !== undefined) { clearTimeout(this.timer); this.timer = undefined; } let clients = this.formatClientsForStanza(); let stanza = { "name": this.serverName, "port": this.serverPort, "hostUsername": Engine.LobbyGetNick(), "mapName": g_GameAttributes.map, "niceMapName": this.mapCache.getTranslatableMapName(g_GameAttributes.mapType, g_GameAttributes.map), "mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default", "mapType": g_GameAttributes.mapType, "victoryConditions": g_GameAttributes.settings.VictoryConditions.join(","), "nbp": clients.connectedPlayers, "maxnbp": g_GameAttributes.settings.PlayerData.length, "players": clients.list, "stunIP": this.stunEndpoint ? this.stunEndpoint.ip : "", "stunPort": this.stunEndpoint ? this.stunEndpoint.port : "", "mods": this.mods }; // Only send the stanza if one of these properties changed if (this.lastStanza && Object.keys(stanza).every(prop => this.lastStanza[prop] == stanza[prop])) return; this.lastStanza = stanza; Engine.SendRegisterGame(stanza); Engine.ProfileStop(); } /** * Send a list of playernames and distinct between players and observers. * Don't send teams, AIs or anything else until the game was started. * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data. */ formatClientsForStanza() { let connectedPlayers = 0; let playerData = []; for (let guid in g_PlayerAssignments) { let pData = { "Name": g_PlayerAssignments[guid].name }; if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1]) ++connectedPlayers; else pData.Team = "observer"; playerData.push(pData); } return { "list": playerDataToStringifiedTeamList(playerData), "connectedPlayers": connectedPlayers }; } } /** * Send the current game settings to the lobby bot if the settings didn't change for this number of milliseconds. */ GameRegisterStanza.prototype.Timeout = 2000; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/SetupWindow.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/SetupWindow.js (revision 24685) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/SetupWindow.js (revision 24686) @@ -1,114 +1,113 @@ /** * This class stores the GameSetupPage and every subpage that is shown in the game setup. */ class SetupWindowPages { } /** * The SetupWindow is the root class owning all other class instances. * The class shall be ineligible to perform any GUI object logic and shall defer that task to owned classes. */ class SetupWindow { constructor(initData, hotloadData) { if (!g_Settings) return; Engine.ProfileStart("SetupWindow"); this.loadHandlers = new Set(); this.closePageHandlers = new Set(); this.getHotloadDataHandlers = new Set(); let netMessages = new NetMessages(this); let startGameControl = new StartGameControl(netMessages); let mapCache = new MapCache(); let mapFilters = new MapFilters(mapCache); let gameSettingsControl = new GameSettingsControl(this, netMessages, startGameControl, mapCache); - let playerAssignmentsControl = new PlayerAssignmentsControl(this, netMessages); + let gameRegisterStanza = Engine.HasXmppClient() && + new GameRegisterStanza(initData, this, netMessages, gameSettingsControl, mapCache); + let playerAssignmentsControl = new PlayerAssignmentsControl(this, netMessages, gameRegisterStanza); let readyControl = new ReadyControl(netMessages, gameSettingsControl, startGameControl, playerAssignmentsControl); // These class instances control central data and do not manage any GUI Object. this.controls = { "gameSettingsControl": gameSettingsControl, "playerAssignmentsControl": playerAssignmentsControl, "mapCache": mapCache, "mapFilters": mapFilters, "readyControl": readyControl, "startGameControl": startGameControl, "netMessages": netMessages, - "gameRegisterStanza": - Engine.HasXmppClient() && - new GameRegisterStanza( - initData, this, netMessages, gameSettingsControl, playerAssignmentsControl, mapCache) + "gameRegisterStanza": gameRegisterStanza }; // These are the pages within the setup window that may use the controls defined above this.pages = {}; for (let name in SetupWindowPages) this.pages[name] = new SetupWindowPages[name](this); setTimeout(displayGamestateNotifications, 1000); Engine.GetGUIObjectByName("setupWindow").onTick = updateTimers; // This event is triggered after all classes have been instantiated and subscribed to each others events for (let handler of this.loadHandlers) handler(initData, hotloadData); Engine.ProfileStop(); if (gameSettingsControl.autostart) startGameControl.launchGame(); } registerLoadHandler(handler) { this.loadHandlers.add(handler); } unregisterLoadHandler(handler) { this.loadHandlers.delete(handler); } registerClosePageHandler(handler) { this.closePageHandlers.add(handler); } unregisterClosePageHandler(handler) { this.closePageHandlers.delete(handler); } registerGetHotloadDataHandler(handler) { this.getHotloadDataHandlers.add(handler); } unregisterGetHotloadDataHandler(handler) { this.getHotloadDataHandlers.delete(handler); } getHotloadData() { let object = {}; for (let handler of this.getHotloadDataHandlers) handler(object); return object; } closePage() { for (let handler of this.closePageHandlers) handler(); if (Engine.HasXmppClient()) Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false }); else Engine.SwitchGuiPage("page_pregame.xml"); } }