Fix undefined guid error when trying to kick as a nonhosting client in the gamesetup...
[0ad.git] / binaries / data / mods / public / gui / gamesetup / gamesetup.js
blob0fbc45f23d9b0a9b920febc3b4e85c84d7ab06ff
1 const g_MatchSettings_SP = "config/matchsettings.json";
2 const g_MatchSettings_MP = "config/matchsettings.mp.json";
4 const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
5 const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly));
6 const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
7 const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
8 const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
9 const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
10 const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions);
11 const g_WonderDurations = prepareForDropdown(g_Settings && g_Settings.WonderDurations);
13 /**
14  * All selectable playercolors except gaia.
15  */
16 const g_PlayerColors = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
18 /**
19  * Directory containing all maps of the given type.
20  */
21 const g_MapPath = {
22         "random": "maps/random/",
23         "scenario": "maps/scenarios/",
24         "skirmish": "maps/skirmishes/"
27 /**
28  * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
29  */
30 const g_NetMessageTypes = {
31         "netstatus": msg => handleNetStatusMessage(msg),
32         "netwarn": msg => addNetworkWarning(msg),
33         "gamesetup": msg => handleGamesetupMessage(msg),
34         "players": msg => handlePlayerAssignmentMessage(msg),
35         "ready": msg => handleReadyMessage(msg),
36         "start": msg => handleGamestartMessage(msg),
37         "kicked": msg => addChatMessage({
38                 "type": msg.banned ? "banned" : "kicked",
39                 "username": msg.username
40         }),
41         "chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text })
44 const g_FormatChatMessage = {
45         "system": (msg, user) => systemMessage(msg.text),
46         "settings": (msg, user) => systemMessage(translate('Game settings have been changed')),
47         "connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })),
48         "disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })),
49         "kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })),
50         "banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })),
51         "chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), {
52                 "username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })),
53                 "message": escapeText(msg.text || "")
54         }),
55         "ready": (msg, user) => sprintf(translate("* %(username)s is ready!"), {
56                 "username": user
57         }),
58         "not-ready": (msg, user) => sprintf(translate("* %(username)s is not ready."), {
59                 "username": user
60         }),
61         "clientlist": (msg, user) => getUsernameList()
64 /**
65  * The dropdownlist items will appear in the order they are added.
66  */
67 const g_MapFilters = [
68         {
69                 "id": "default",
70                 "name": translate("Default"),
71                 "filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1)
72         },
73         {
74                 "id": "naval",
75                 "name": translate("Naval Maps"),
76                 "filter": mapKeywords => mapKeywords.indexOf("naval") != -1
77         },
78         {
79                 "id": "demo",
80                 "name": translate("Demo Maps"),
81                 "filter": mapKeywords => mapKeywords.indexOf("demo") != -1
82         },
83         {
84                 "id": "new",
85                 "name": translate("New Maps"),
86                 "filter": mapKeywords => mapKeywords.indexOf("new") != -1
87         },
88         {
89                 "id": "trigger",
90                 "name": translate("Trigger Maps"),
91                 "filter": mapKeywords => mapKeywords.indexOf("trigger") != -1
92         },
93         {
94                 "id": "all",
95                 "name": translate("All Maps"),
96                 "filter": mapKeywords => true
97         }
101  * Used for generating the botnames.
102  */
103 const g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
106  * Offer users to select playable civs only.
107  * Load unselectable civs as they could appear in scenario maps.
108  */
109 const g_CivData = loadCivData();
112  * Used for highlighting the sender of chat messages.
113  */
114 const g_SenderFont = "sans-bold-13";
117  * Highlight the "random" dropdownlist item.
118  */
119 const g_ColorRandom = "orange";
122  * Highlight AIs in the player-dropdownlist.
123  */
124 const g_AIColor = "70 150 70";
127  * Color for "Unassigned"-placeholder item in the dropdownlist.
128  */
129 const g_UnassignedColor = "140 140 140";
132  * Highlight observer players in the dropdownlist.
133  */
134 const g_UnassignedPlayerColor = "170 170 250";
137  * Highlight ready players.
138  */
139 const g_ReadyColor = "green";
142  * Placeholder item for the map-dropdownlist.
143  */
144 const g_RandomMap = '[color="' + g_ColorRandom + '"]' + translateWithContext("map selection", "Random") + "[/color]";
147  * Placeholder item for the civ-dropdownlists.
148  */
149 const g_RandomCiv = '[color="' + g_ColorRandom + '"]' + translateWithContext("civilization", "Random") + '[/color]';
152  * Whether this is a single- or multiplayer match.
153  */
154 var g_IsNetworked;
157  * Is this user in control of game settings (i.e. singleplayer or host of a multiplayergame).
158  */
159 var g_IsController;
162  * To report the game to the lobby bot.
163  */
164 var g_ServerName;
165 var g_ServerPort;
168  * States whether the GUI is currently updated in response to network messages instead of user input
169  * and therefore shouldn't send further messages to the network.
170  */
171 var g_IsInGuiUpdate;
174  * Whether the current player is ready to start the game.
175  */
176 var g_IsReady;
179  * Ignore duplicate ready commands on init.
180  */
181 var g_ReadyInit = true;
184  * If noone has changed the ready status, we have no need to spam the settings changed message.
186  * <=0 - Suppressed settings message
187  * 1 - Will show settings message
188  * 2 - Host's initial ready, suppressed settings message
189  */
190 var g_ReadyChanged = 2;
193  * Used to prevent calling resetReadyData when starting a game.
194  */
195 var g_GameStarted = false;
197 var g_PlayerAssignments = {};
199 var g_DefaultPlayerData = [];
201 var g_GameAttributes = { "settings": {} };
203 var g_ChatMessages = [];
206  * Cache containing the mapsettings for scenario/skirmish maps. Just-in-time loading.
207  */
208 var g_MapData = {};
211  * Wait one tick before initializing the GUI objects and
212  * don't process netmessages prior to that.
213  */
214 var g_LoadingState = 0;
217  * Only send a lobby update if something actually changed.
218  */
219 var g_LastGameStanza;
222  * Remembers if the current player viewed the AI settings of some playerslot.
223  */
224 var g_LastViewedAIPlayer = -1;
227  * Initializes some globals without touching the GUI.
229  * @param {Object} attribs - context data sent by the lobby / mainmenu
230  */
231 function init(attribs)
233         if (!g_Settings)
234         {
235                 cancelSetup();
236                 return;
237         }
239         if (["offline", "server", "client"].indexOf(attribs.type) == -1)
240         {
241                 error("Unexpected 'type' in gamesetup init: " + attribs.type);
242                 cancelSetup();
243                 return;
244         }
246         g_IsNetworked = attribs.type != "offline";
247         g_IsController = attribs.type != "client";
248         g_ServerName = attribs.serverName;
249         g_ServerPort = attribs.serverPort;
251         // Replace empty playername when entering a singleplayermatch for the first time
252         if (!g_IsNetworked)
253         {
254                 Engine.ConfigDB_CreateValue("user", "playername.singleplayer", singleplayerName());
255                 Engine.ConfigDB_WriteValueToFile("user", "playername.singleplayer", singleplayerName(), "config/user.cfg");
256         }
258         // Get default player data - remove gaia
259         g_DefaultPlayerData = g_Settings.PlayerDefaults;
260         g_DefaultPlayerData.shift();
261         for (let i in g_DefaultPlayerData)
262                 g_DefaultPlayerData[i].Civ = "random";
264         setTimeout(displayGamestateNotifications, 1000);
268  * Called after the first tick.
269  */
270 function initGUIObjects()
272         Engine.GetGUIObjectByName("cancelGame").tooltip = Engine.HasXmppClient() ? translate("Return to the lobby.") : translate("Return to the main menu.");
274         initCivNameList();
275         initMapTypes();
276         initMapFilters();
278         if (g_IsController)
279         {
280                 g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked;
281                 g_GameAttributes.settings.RatingEnabled = Engine.IsRankedGame() || undefined;
283                 initMapNameList();
284                 initNumberOfPlayers();
285                 initGameSpeed();
286                 initPopulationCaps();
287                 initStartingResources();
288                 initCeasefire();
289                 initWonderDurations();
290                 initVictoryConditions();
291                 initMapSizes();
292                 initRadioButtons();
293         }
294         else
295                 hideControls();
297         initMultiplayerSettings();
298         initPlayerAssignments();
300         resizeMoreOptionsWindow();
302         Engine.GetGUIObjectByName("chatInput").tooltip = colorizeAutocompleteHotkey();
304         if (g_IsNetworked)
305                 Engine.GetGUIObjectByName("chatInput").focus();
307         if (g_IsController)
308         {
309                 loadPersistMatchSettings();
310                 if (g_IsInGuiUpdate)
311                         warn("initGUIObjects() called while in GUI update");
312                 updateGameAttributes();
313         }
316 function initMapTypes()
318         let mapTypes = Engine.GetGUIObjectByName("mapType");
319         mapTypes.list = g_MapTypes.Title;
320         mapTypes.list_data = g_MapTypes.Name;
321         mapTypes.onSelectionChange = function() {
322                 if (this.selected != -1)
323                         selectMapType(this.list_data[this.selected]);
324         };
325         if (g_IsController)
326                 mapTypes.selected = g_MapTypes.Default;
329 function initMapFilters()
331         let mapFilters = Engine.GetGUIObjectByName("mapFilter");
332         mapFilters.list = g_MapFilters.map(mapFilter => mapFilter.name);
333         mapFilters.list_data = g_MapFilters.map(mapFilter => mapFilter.id);
334         mapFilters.onSelectionChange = function() {
335                 if (this.selected != -1)
336                         selectMapFilter(this.list_data[this.selected]);
337         };
338         if (g_IsController)
339                 mapFilters.selected = 0;
340         g_GameAttributes.mapFilter = "default";
344  * Remove empty space in case of hidden options (like cheats, rating or wonder duration)
345  */
346 function resizeMoreOptionsWindow()
348         const elementHeight = 30;
350         let yPos = undefined;
352         for (let guiOption of Engine.GetGUIObjectByName("moreOptions").children)
353         {
354                 if (guiOption.name == "moreOptionsLabel")
355                         continue;
357                 let gSize = guiOption.size;
358                 yPos = yPos || gSize.top;
360                 if (guiOption.hidden)
361                         continue;
363                 gSize.top = yPos;
364                 gSize.bottom = yPos + elementHeight - 2;
365                 guiOption.size = gSize;
367                 yPos += elementHeight;
368         }
370         // Resize the vertically centered window containing the options
371         let moreOptions = Engine.GetGUIObjectByName("moreOptions");
372         let mSize = moreOptions.size;
373         mSize.bottom = mSize.top + yPos + 20;
374         moreOptions.size = mSize;
377 function initNumberOfPlayers()
379         let playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ..., MaxPlayers
380         let numPlayers = Engine.GetGUIObjectByName("numPlayers");
381         numPlayers.list = playersArray;
382         numPlayers.list_data = playersArray;
383         numPlayers.onSelectionChange = function() {
384                 if (this.selected != -1)
385                         selectNumPlayers(this.list_data[this.selected]);
386         };
387         numPlayers.selected = g_MaxPlayers - 1;
390 function initGameSpeed()
392         let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
393         gameSpeed.hidden = false;
394         Engine.GetGUIObjectByName("gameSpeedText").hidden = true;
395         gameSpeed.list = g_GameSpeeds.Title;
396         gameSpeed.list_data = g_GameSpeeds.Speed;
397         gameSpeed.onSelectionChange = function() {
398                 if (this.selected != -1)
399                         g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[this.selected];
401                 updateGameAttributes();
402         };
403         gameSpeed.selected = g_GameSpeeds.Default;
406 function initPopulationCaps()
408         let populationCaps = Engine.GetGUIObjectByName("populationCap");
409         populationCaps.list = g_PopulationCapacities.Title;
410         populationCaps.list_data = g_PopulationCapacities.Population;
411         populationCaps.selected = g_PopulationCapacities.Default;
412         populationCaps.onSelectionChange = function() {
413                 if (this.selected != -1)
414                         g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[this.selected];
416                 updateGameAttributes();
417         };
420 function initStartingResources()
422         let startingResourcesL = Engine.GetGUIObjectByName("startingResources");
423         startingResourcesL.list = g_StartingResources.Title;
424         startingResourcesL.list_data = g_StartingResources.Resources;
425         startingResourcesL.selected = g_StartingResources.Default;
426         startingResourcesL.onSelectionChange = function() {
427                 if (this.selected != -1)
428                         g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[this.selected];
430                 updateGameAttributes();
431         };
434 function initCeasefire()
436         let ceasefireL = Engine.GetGUIObjectByName("ceasefire");
437         ceasefireL.list = g_Ceasefire.Title;
438         ceasefireL.list_data = g_Ceasefire.Duration;
439         ceasefireL.selected = g_Ceasefire.Default;
440         ceasefireL.onSelectionChange = function() {
441                 if (this.selected != -1)
442                         g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[this.selected];
444                 updateGameAttributes();
445         };
448 function initVictoryConditions()
450         let victoryConditions = Engine.GetGUIObjectByName("victoryCondition");
451         victoryConditions.list = g_VictoryConditions.Title;
452         victoryConditions.list_data = g_VictoryConditions.Name;
453         victoryConditions.onSelectionChange = function() {
454                 if (this.selected != -1)
455                 {
456                         g_GameAttributes.settings.GameType = g_VictoryConditions.Name[this.selected];
457                         g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[this.selected];
458                 }
460                 updateGameAttributes();
461         };
462         victoryConditions.selected = g_VictoryConditions.Default;
465 function initWonderDurations()
467         let wonderConditions = Engine.GetGUIObjectByName("wonderDuration");
468         wonderConditions.list = g_WonderDurations.Title;
469         wonderConditions.list_data = g_WonderDurations.Duration;
470         wonderConditions.onSelectionChange = function()
471         {
472                 if (this.selected != -1)
473                         g_GameAttributes.settings.WonderDuration = g_WonderDurations.Duration[this.selected];
475                 updateGameAttributes();
476         };
477         wonderConditions.selected = g_WonderDurations.Default;
480 function initMapSizes()
482         let mapSize = Engine.GetGUIObjectByName("mapSize");
483         mapSize.list = g_MapSizes.Name;
484         mapSize.list_data = g_MapSizes.Tiles;
485         mapSize.onSelectionChange = function() {
486                 if (this.selected != -1)
487                         g_GameAttributes.settings.Size = g_MapSizes.Tiles[this.selected];
488                 updateGameAttributes();
489         };
490         mapSize.selected = 0;
494  * Assign update-functions to all checkboxes.
495  */
496 function initRadioButtons()
498         let options = {
499                 "RevealMap": "revealMap",
500                 "ExploreMap": "exploreMap",
501                 "DisableTreasures": "disableTreasures",
502                 "LockTeams": "lockTeams",
503                 "LastManStanding" : "lastManStanding",
504                 "CheatsEnabled": "enableCheats"
505         };
507         Object.keys(options).forEach(attribute => {
508                 Engine.GetGUIObjectByName(options[attribute]).onPress = function() {
509                         g_GameAttributes.settings[attribute] = this.checked;
510                         updateGameAttributes();
511                 };
512         });
514         Engine.GetGUIObjectByName("enableRating").onPress = function() {
515                 g_GameAttributes.settings.RatingEnabled = this.checked;
516                 Engine.SetRankedGame(this.checked);
517                 Engine.GetGUIObjectByName("enableCheats").enabled = !this.checked;
518                 Engine.GetGUIObjectByName("lockTeams").enabled = !this.checked;
519                 updateGameAttributes();
520         };
522         Engine.GetGUIObjectByName("lockTeams").onPress = function() {
523                 g_GameAttributes.settings.LockTeams = this.checked;
524                 g_GameAttributes.settings.LastManStanding = false;
525                 updateGameAttributes();
526         };
529 function hideStartGameButton(hidden)
531         const offset = 10;
533         let startGame = Engine.GetGUIObjectByName("startGame");
534         startGame.hidden = hidden;
535         let right = hidden ? startGame.size.right : startGame.size.left - offset;
537         let cancelGame = Engine.GetGUIObjectByName("cancelGame");
538         let cancelGameSize = cancelGame.size;
539         let xButtonSize = cancelGameSize.right - cancelGameSize.left;
540         cancelGameSize.right = right;
541         right -= xButtonSize;
543         for (let element of ["cheatWarningText", "onscreenToolTip"])
544         {
545                 let elementSize = Engine.GetGUIObjectByName(element).size;
546                 elementSize.right = right - (cancelGameSize.left - elementSize.right);
547                 Engine.GetGUIObjectByName(element).size = elementSize;
548         }
550         cancelGameSize.left = right;
551         cancelGame.size = cancelGameSize;
555  * If we're a network client, hide the controls and show the text instead.
556  */
557 function hideControls()
559         for (let ctrl of ["mapType", "mapFilter", "mapSelection", "victoryCondition", "gameSpeed", "numPlayers"])
560                 hideControl(ctrl, ctrl + "Text");
562         // TODO: Shouldn't players be able to choose their own assignment?
563         for (let i = 0; i < g_MaxPlayers; ++i)
564         {
565                 Engine.GetGUIObjectByName("playerAssignment["+i+"]").hidden = true;
566                 Engine.GetGUIObjectByName("playerCiv["+i+"]").hidden = true;
567                 Engine.GetGUIObjectByName("playerTeam["+i+"]").hidden = true;
568         }
570         // The start game button should be hidden until the player assignments are received
571         // and it is known whether the local player is an observer.
572         hideStartGameButton(true);
573         Engine.GetGUIObjectByName("startGame").enabled = true;
577  * Hides the GUI controls for clients and shows the read-only label instead.
579  * @param {string} control - name of the GUI object able to change a setting
580  * @param {string} label - name of the GUI object displaying a setting
581  * @param {boolean} [allowControl] - Whether the current user is allowed to change the control.
582  */
583 function hideControl(control, label, allowControl = g_IsController)
585         Engine.GetGUIObjectByName(control).hidden = !allowControl;
586         Engine.GetGUIObjectByName(label).hidden = allowControl;
590  * Checks a boolean checkbox for the host and sets the text of the label for the client.
592  * @param {string} control - name of the GUI object able to change a setting
593  * @param {string} label - name of the GUI object displaying a setting
594  * @param {boolean} checked - Whether the setting is active / enabled.
595  */
596 function setGUIBoolean(control, label, checked)
598         Engine.GetGUIObjectByName(control).checked = checked;
599         Engine.GetGUIObjectByName(label).caption = checked ? translate("Yes") : translate("No");
603  * Hide and set some elements depending on whether we play single- or multiplayer.
604  */
605 function initMultiplayerSettings()
607         Engine.GetGUIObjectByName("chatPanel").hidden = !g_IsNetworked;
608         Engine.GetGUIObjectByName("optionCheats").hidden = !g_IsNetworked;
609         Engine.GetGUIObjectByName("optionRating").hidden = !Engine.HasXmppClient();
611         Engine.GetGUIObjectByName("enableCheats").enabled = !Engine.IsRankedGame();
612         Engine.GetGUIObjectByName("lockTeams").enabled = !Engine.IsRankedGame();
614         Engine.GetGUIObjectByName("enableCheats").checked = g_GameAttributes.settings.CheatsEnabled;
615         Engine.GetGUIObjectByName("enableRating").checked = !!g_GameAttributes.settings.RatingEnabled;
617         for (let ctrl of ["enableCheats", "enableRating"])
618                 hideControl(ctrl, ctrl + "Text");
622  * Populate team-, color- and civ-dropdowns.
623  */
624 function initPlayerAssignments()
626         let boxSpacing = 32;
627         for (let i = 0; i < g_MaxPlayers; ++i)
628         {
629                 let box = Engine.GetGUIObjectByName("playerBox["+i+"]");
630                 let boxSize = box.size;
631                 let h = boxSize.bottom - boxSize.top;
632                 boxSize.top = i * boxSpacing;
633                 boxSize.bottom = i * boxSpacing + h;
634                 box.size = boxSize;
636                 let team = Engine.GetGUIObjectByName("playerTeam["+i+"]");
637                 let teamsArray = Array(g_MaxTeams).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxTeams
638                 team.list = [translateWithContext("team", "None")].concat(teamsArray); // "None", 1, 2, ..., maxTeams
639                 team.list_data = [-1].concat(teamsArray.map(team => team - 1)); // -1, 0, ..., (maxTeams-1)
640                 team.selected = 0;
642                 let playerSlot = i;     // declare for inner function use
643                 team.onSelectionChange = function() {
644                         if (this.selected != -1)
645                                 g_GameAttributes.settings.PlayerData[playerSlot].Team = this.selected - 1;
647                         updateGameAttributes();
648                 };
650                 let colorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
651                 colorPicker.list = g_PlayerColors.map(color => ' ' + '[color="' + rgbToGuiColor(color) + '"]â– [/color]');
652                 colorPicker.list_data = g_PlayerColors.map((color, index) => index);
653                 colorPicker.selected = -1;
654                 colorPicker.onSelectionChange = function() { selectPlayerColor(playerSlot, this.selected); };
656                 Engine.GetGUIObjectByName("playerCiv["+i+"]").onSelectionChange = function() {
657                         if ((this.selected != -1)&&(g_GameAttributes.mapType !== "scenario"))
658                                 g_GameAttributes.settings.PlayerData[playerSlot].Civ = this.list_data[this.selected];
660                         updateGameAttributes();
661                 };
662         }
666  * Called when the client disconnects.
667  * The other cases from NetClient should never occur in the gamesetup.
668  * @param {Object} message
669  */
670 function handleNetStatusMessage(message)
672         if (message.status != "disconnected")
673         {
674                 error("Unrecognised netstatus type " + message.status);
675                 return;
676         }
678         cancelSetup();
679         reportDisconnect(message.reason, true);
683  * Called whenever a client clicks on ready (or not ready).
684  * @param {Object} message
685  */
686 function handleReadyMessage(message)
688         --g_ReadyChanged;
690         if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
691                 addChatMessage({
692                         "type": message.status == 1 ? "ready" : "not-ready",
693                         "guid": message.guid
694                 });
696         if (!g_IsController)
697                 return;
699         g_PlayerAssignments[message.guid].status = +message.status == 1;
700         updateReadyUI();
704  * Called after every player is ready and the host decided to finally start the game.
705  * @param {Object} message
706  */
707 function handleGamestartMessage(message)
709         // Immediately inform the lobby server instead of waiting for the load to finish
710         if (g_IsController && Engine.HasXmppClient())
711         {
712                 let clients = formatClientsForStanza();
713                 Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
714         }
716         Engine.SwitchGuiPage("page_loading.xml", {
717                 "attribs": g_GameAttributes,
718                 "isNetworked" : g_IsNetworked,
719                 "playerAssignments": g_PlayerAssignments,
720                 "isController": g_IsController
721         });
725  * Called whenever the host changed any setting.
726  * @param {Object} message
727  */
728 function handleGamesetupMessage(message)
730         if (!message.data)
731                 return;
733         g_GameAttributes = message.data;
735         if (!!g_GameAttributes.settings.RatingEnabled)
736         {
737                 g_GameAttributes.settings.CheatsEnabled = false;
738                 g_GameAttributes.settings.LockTeams = true;
739                 g_GameAttributes.settings.LastManStanding = false;
740         }
742         Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
744         updateGUIObjects();
748  * Called whenever a client joins/leaves or any gamesetting is changed.
749  * @param {Object} message
750  */
751 function handlePlayerAssignmentMessage(message)
753         for (let guid in message.newAssignments)
754                 if (!g_PlayerAssignments[guid])
755                         onClientJoin(guid, message.newAssignments);
757         for (let guid in g_PlayerAssignments)
758                 if (!message.newAssignments[guid])
759                         onClientLeave(guid);
761         g_PlayerAssignments = message.newAssignments;
763         hideStartGameButton(!g_IsController && g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1);
765         updatePlayerList();
766         updateReadyUI();
767         sendRegisterGameStanza();
770 function onClientJoin(newGUID, newAssignments)
772         addChatMessage({
773                 "type": "connect",
774                 "guid": newGUID,
775                 "username": newAssignments[newGUID].name
776         });
778         // Assign joining observers to unused player numbers
779         if (!g_IsController || newAssignments[newGUID].player != -1)
780                 return;
782         let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v,i) =>
783                 Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i+1)
784         );
786         if (freeSlot == -1)
787                 return;
789         Engine.AssignNetworkPlayer(freeSlot + 1, newGUID);
790         resetReadyData();
793 function onClientLeave(guid)
795         addChatMessage({
796                 "type": "disconnect",
797                 "guid": guid
798         });
800         if (g_PlayerAssignments[guid].player != -1)
801                 resetReadyData();
805  * Doesn't translate, so that lobby clients can do that locally
806  * (even if they don't have that map).
807  */
808 function getMapDisplayName(map)
810         if (map == "random")
811                 return map;
813         let mapData = loadMapData(map);
814         if (!mapData || !mapData.settings || !mapData.settings.Name)
815                 return map;
817         return mapData.settings.Name;
820 function getMapPreview(map)
822         let mapData = loadMapData(map);
823         if (!mapData || !mapData.settings || !mapData.settings.Preview)
824                 return "nopreview.png";
826         return mapData.settings.Preview;
830  * Get a playersetting or return the default if it wasn't set.
831  */
832 function getSetting(settings, defaults, property)
834         if (settings && (property in settings))
835                 return settings[property];
837         if (defaults && (property in defaults))
838                 return defaults[property];
840         return undefined;
844  * Initialize the dropdowns containing all selectable civs (including random).
845  */
846 function initCivNameList()
848         let civList = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => ({ "name": g_CivData[civ].Name, "code": civ })).sort(sortNameIgnoreCase);
849         let civListNames = [g_RandomCiv].concat(civList.map(civ => civ.name));
850         let civListCodes = ["random"].concat(civList.map(civ => civ.code));
852         for (let i = 0; i < g_MaxPlayers; ++i)
853         {
854                 let civ = Engine.GetGUIObjectByName("playerCiv["+i+"]");
855                 civ.list = civListNames;
856                 civ.list_data = civListCodes;
857                 civ.selected = 0;
858         }
862  * Initialize the dropdown containing all maps for the selected maptype and mapfilter.
863  */
864 function initMapNameList()
866         if (!g_MapPath[g_GameAttributes.mapType])
867         {
868                 error("Unexpected map type: " + g_GameAttributes.mapType);
869                 return;
870         }
872         let mapFiles = g_GameAttributes.mapType == "random" ?
873                 getJSONFileList(g_GameAttributes.mapPath) :
874                 getXMLFileList(g_GameAttributes.mapPath);
876         // Apply map filter, if any defined
877         // TODO: Should verify these are valid maps before adding to list
878         let mapList = [];
879         for (let mapFile of mapFiles)
880         {
881                 let file = g_GameAttributes.mapPath + mapFile;
882                 let mapData = loadMapData(file);
883                 let mapFilter = g_MapFilters.find(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "all"));
885                 if (!!mapData.settings && mapFilter && mapFilter.filter(mapData.settings.Keywords || []))
886                         mapList.push({ "name": getMapDisplayName(file), "file": file });
887         }
889         translateObjectKeys(mapList, ["name"]);
890         mapList.sort(sortNameIgnoreCase);
892         let mapListNames = mapList.map(map => map.name);
893         let mapListFiles = mapList.map(map => map.file);
895         if (g_GameAttributes.mapType == "random")
896         {
897                 mapListNames.unshift(g_RandomMap);
898                 mapListFiles.unshift("random");
899         }
901         let mapSelectionBox = Engine.GetGUIObjectByName("mapSelection");
902         mapSelectionBox.list = mapListNames;
903         mapSelectionBox.list_data = mapListFiles;
904         mapSelectionBox.onSelectionChange = function() {
905                 if (this.list_data[this.selected])
906                         selectMap(this.list_data[this.selected]);
907         };
908         mapSelectionBox.selected = Math.max(0, mapListFiles.indexOf(g_GameAttributes.map || ""));
911 function loadMapData(name)
913         if (!name || !g_MapPath[g_GameAttributes.mapType])
914                 return undefined;
916         if (name == "random")
917                 return { "settings": { "Name": "", "Description": "" } };
919         if (!g_MapData[name])
920                 g_MapData[name] = g_GameAttributes.mapType == "random" ?
921                         Engine.ReadJSONFile(name + ".json") :
922                         Engine.LoadMapSettings(name);
924         return g_MapData[name];
928  * Sets the gameattributes the way they were the last time the user left the gamesetup.
929  */
930 function loadPersistMatchSettings()
932         if (Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true")
933                 return;
935         let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
936         if (!Engine.FileExists(settingsFile))
937                 return;
939         let attrs = Engine.ReadJSONFile(settingsFile);
940         if (!attrs || !attrs.settings)
941                 return;
943         g_IsInGuiUpdate = true;
945         let mapName = attrs.map || "";
946         let mapSettings = attrs.settings;
948         g_GameAttributes = attrs;
950         if (!g_IsNetworked)
951                 mapSettings.CheatsEnabled = true;
953         // Replace unselectable civs with random civ
954         let playerData = mapSettings.PlayerData;
955         if (playerData && g_GameAttributes.mapType != "scenario")
956                 for (let i in playerData)
957                         if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup)
958                                 playerData[i].Civ = "random";
960         // Apply map settings
961         let newMapData = loadMapData(mapName);
962         if (newMapData && newMapData.settings)
963         {
964                 for (let prop in newMapData.settings)
965                         mapSettings[prop] = newMapData.settings[prop];
967                 if (playerData)
968                         mapSettings.PlayerData = playerData;
969         }
971         if (mapSettings.PlayerData)
972                 sanitizePlayerData(mapSettings.PlayerData);
974         // Reload, as the maptype or mapfilter might have changed
975         initMapNameList();
977         g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
978         Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
980         updateGUIObjects();
983 function savePersistMatchSettings()
985         let attributes = Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {};
986         Engine.WriteJSONFile(g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, attributes);
989 function sanitizePlayerData(playerData)
991         // Remove gaia
992         if (playerData.length && !playerData[0])
993                 playerData.shift();
995         playerData.forEach((pData, index) => {
996                 pData.Color = pData.Color || g_PlayerColors[index];
997                 pData.Civ = pData.Civ || "random";
999                 // Use default AI if the map doesn't specify any explicitly
1000                 if (!("AI" in pData))
1001                         pData.AI = g_DefaultPlayerData[index].AI;
1003                 if (!("AIDiff" in pData))
1004                         pData.AIDiff = g_DefaultPlayerData[index].AIDiff;
1005         });
1007         // Replace colors with the best matching color of PlayerDefaults
1008         if (g_GameAttributes.mapType != "scenario")
1009         {
1010                 playerData.forEach((pData, index) => {
1011                         let colorDistances = g_PlayerColors.map(color => colorDistance(color, pData.Color));
1012                         let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance)));
1013                         pData.Color = g_PlayerColors.find(color => colorDistance(color, pData.Color) == smallestDistance);
1014                 });
1015         }
1017         ensureUniquePlayerColors(playerData);
1020 function cancelSetup()
1022         if (g_IsController)
1023                 savePersistMatchSettings();
1025         Engine.DisconnectNetworkGame();
1027         if (Engine.HasXmppClient())
1028         {
1029                 Engine.LobbySetPlayerPresence("available");
1031                 if (g_IsController)
1032                         Engine.SendUnregisterGame();
1034                 Engine.SwitchGuiPage("page_lobby.xml");
1035         }
1036         else
1037                 Engine.SwitchGuiPage("page_pregame.xml");
1041  * Can't init the GUI before the first tick.
1042  * Process netmessages afterwards.
1043  */
1044 function onTick()
1046         if (!g_Settings)
1047                 return;
1049         // First tick happens before first render, so don't load yet
1050         if (g_LoadingState == 0)
1051         {
1052                 ++g_LoadingState;
1053         }
1054         else if (g_LoadingState == 1)
1055         {
1056                 Engine.GetGUIObjectByName("loadingWindow").hidden = true;
1057                 Engine.GetGUIObjectByName("setupWindow").hidden = false;
1058                 initGUIObjects();
1059                 ++g_LoadingState;
1060         }
1061         else if (g_LoadingState == 2)
1062         {
1063                 while (true)
1064                 {
1065                         let message = Engine.PollNetworkClient();
1066                         if (!message)
1067                                 break;
1069                         log("Net message: " + uneval(message));
1071                         if (g_NetMessageTypes[message.type])
1072                                 g_NetMessageTypes[message.type](message);
1073                         else
1074                                 error("Unrecognised net message type " + message.type);
1075                 }
1076         }
1078         updateTimers();
1082  * Called when the map or the number of players changes.
1083  */
1084 function unassignInvalidPlayers(maxPlayers)
1086         if (g_IsNetworked)
1087         {
1088                 // Remove invalid playerIDs from the servers playerassignments copy
1089                 for (let playerID = +maxPlayers + 1; playerID <= g_MaxPlayers; ++playerID)
1090                         Engine.AssignNetworkPlayer(playerID, "");
1091         }
1092         else if (!g_PlayerAssignments.local ||
1093                  g_PlayerAssignments.local.player > maxPlayers)
1094                 g_PlayerAssignments = {
1095                         "local": {
1096                                 "name": singleplayerName(),
1097                                 "player": 1
1098                         }
1099                 };
1103  * Called when the host choses the number of players on a random map.
1104  * @param {Number} num
1105  */
1106 function selectNumPlayers(num)
1108         if (g_IsInGuiUpdate || !g_IsController || g_GameAttributes.mapType != "random")
1109                 return;
1111         let pData = g_GameAttributes.settings.PlayerData;
1112         g_GameAttributes.settings.PlayerData =
1113                 num > pData.length ?
1114                         pData.concat(g_DefaultPlayerData.slice(pData.length, num)) :
1115                         pData.slice(0, num);
1117         unassignInvalidPlayers(num);
1119         sanitizePlayerData(g_GameAttributes.settings.PlayerData);
1121         updateGameAttributes();
1125  * Assigns the given color to that player.
1126  */
1127 function selectPlayerColor(playerSlot, colorIndex)
1129         if (colorIndex == -1)
1130                 return;
1132         let playerData = g_GameAttributes.settings.PlayerData;
1134         // If someone else has that color, give that player the old color
1135         let pData = playerData.find(pData => sameColor(g_PlayerColors[colorIndex], pData.Color));
1136         if (pData)
1137                 pData.Color = playerData[playerSlot].Color;
1139         // Assign the new color
1140         playerData[playerSlot].Color = g_PlayerColors[colorIndex];
1142         // Ensure colors are not used twice after increasing the number of players
1143         ensureUniquePlayerColors(playerData);
1145         if (!g_IsInGuiUpdate)
1146                 updateGameAttributes();
1149 function ensureUniquePlayerColors(playerData)
1151         for (let i = playerData.length - 1; i >= 0; --i)
1152                 // If someone else has that color, assign an unused color
1153                 if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color)))
1154                         playerData[i].Color = g_PlayerColors.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
1158  * Called when the user selects a map type from the list.
1160  * @param {string} type - scenario, skirmish or random
1161  */
1162 function selectMapType(type)
1164         if (g_IsInGuiUpdate || !g_IsController)
1165                 return;
1167         if (!g_MapPath[type])
1168         {
1169                 error("selectMapType: Unexpected map type " + type);
1170                 return;
1171         }
1173         g_MapData = {};
1174         g_GameAttributes.map = "";
1175         g_GameAttributes.mapType = type;
1176         g_GameAttributes.mapPath = g_MapPath[type];
1178         if (type != "scenario")
1179                 g_GameAttributes.settings = {
1180                         "PlayerData": g_DefaultPlayerData.slice(0, 4),
1181                         "CheatsEnabled": g_GameAttributes.settings.CheatsEnabled
1182                 };
1184         initMapNameList();
1186         updateGameAttributes();
1189 function selectMapFilter(id)
1191         if (g_IsInGuiUpdate || !g_IsController)
1192                 return;
1194         g_GameAttributes.mapFilter = id;
1196         initMapNameList();
1198         updateGameAttributes();
1201 function selectMap(name)
1203         if (g_IsInGuiUpdate || !g_IsController || !name)
1204                 return;
1206         // Reset some map specific properties which are not necessarily redefined on each map
1207         for (let prop of ["TriggerScripts", "CircularMap", "Garrison"])
1208                 g_GameAttributes.settings[prop] = undefined;
1210         let mapData = loadMapData(name);
1211         let mapSettings = mapData && mapData.settings ? deepcopy(mapData.settings) : {};
1213         // Reset victory conditions
1214         if (g_GameAttributes.mapType != "random")
1215         {
1216                 let victoryIdx = g_VictoryConditions.Name.indexOf(mapSettings.GameType || "") != -1 ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
1217                 g_GameAttributes.settings.GameType = g_VictoryConditions.Name[victoryIdx];
1218                 g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[victoryIdx];
1219         }
1221         if (g_GameAttributes.mapType == "scenario")
1222         {
1223                 delete g_GameAttributes.settings.WonderDuration;
1224                 delete g_GameAttributes.settings.LastManStanding;
1225         }
1227         if (mapSettings.PlayerData)
1228                 sanitizePlayerData(mapSettings.PlayerData);
1230         // Copy any new settings
1231         g_GameAttributes.map = name;
1232         g_GameAttributes.script = mapSettings.Script;
1233         if (g_GameAttributes.map !== "random")
1234                 for (let prop in mapSettings)
1235                         g_GameAttributes.settings[prop] = mapSettings[prop];
1237         unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length);
1239         updateGameAttributes();
1242 function launchGame()
1244         if (!g_IsController)
1245         {
1246                 error("Only host can start game");
1247                 return;
1248         }
1250         if (!g_GameAttributes.map)
1251                 return;
1253         savePersistMatchSettings();
1255         // Select random map
1256         if (g_GameAttributes.map == "random")
1257         {
1258                 let victoryScriptsSelected = g_GameAttributes.settings.VictoryScripts;
1259                 let gameTypeSelected = g_GameAttributes.settings.GameType;
1260                 selectMap(Engine.GetGUIObjectByName("mapSelection").list_data[Math.floor(Math.random() *
1261                         (Engine.GetGUIObjectByName("mapSelection").list.length - 1)) + 1]);
1262                 g_GameAttributes.settings.VictoryScripts = victoryScriptsSelected;
1263                 g_GameAttributes.settings.GameType = gameTypeSelected;
1264         }
1266         g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []);
1268         // Prevent reseting the readystate
1269         g_GameStarted = true;
1271         g_GameAttributes.settings.mapType = g_GameAttributes.mapType;
1273         // Get a unique array of selectable cultures
1274         let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture);
1275         cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index);
1277         // Determine random civs and botnames
1278         for (let i in g_GameAttributes.settings.PlayerData)
1279         {
1280                 // Pick a random civ of a random culture
1281                 let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random";
1282                 if (chosenCiv == "random")
1283                 {
1284                         let culture = cultures[Math.floor(Math.random() * cultures.length)];
1285                         let civs = Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture);
1286                         chosenCiv = civs[Math.floor(Math.random() * civs.length)];
1287                 }
1288                 g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv;
1290                 // Pick one of the available botnames for the chosen civ
1291                 if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI)
1292                         continue;
1294                 let chosenName = g_CivData[chosenCiv].AINames[Math.floor(Math.random() * g_CivData[chosenCiv].AINames.length)];
1296                 if (!g_IsNetworked)
1297                         chosenName = translate(chosenName);
1299                 // Count how many players use the chosenName
1300                 let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length;
1302                 g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName : sprintf(translate("%(playerName)s %(romanNumber)s"), { "playerName": chosenName, "romanNumber": g_RomanNumbers[usedName+1] });
1303         }
1305         // Copy playernames for the purpose of replays
1306         for (let guid in g_PlayerAssignments)
1307         {
1308                 let player = g_PlayerAssignments[guid];
1309                 if (player.player > 0)  // not observer or GAIA
1310                         g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
1311         }
1313         // Seed used for both map generation and simulation
1314         g_GameAttributes.settings.Seed = Math.floor(Math.random() * Math.pow(2, 32));
1315         g_GameAttributes.settings.AISeed = Math.floor(Math.random() * Math.pow(2, 32));
1317         // Used for identifying rated game reports for the lobby
1318         g_GameAttributes.matchID = Engine.GetMatchID();
1320         if (g_IsNetworked)
1321         {
1322                 Engine.SetNetworkGameAttributes(g_GameAttributes);
1323                 Engine.StartNetworkGame();
1324         }
1325         else
1326         {
1327                 // Find the player ID which the user has been assigned to
1328                 let playerID = -1;
1329                 for (let i in g_GameAttributes.settings.PlayerData)
1330                 {
1331                         let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
1332                         if (assignBox.list_data[assignBox.selected] == "local")
1333                                 playerID = +i+1;
1334                 }
1336                 Engine.StartGame(g_GameAttributes, playerID);
1337                 Engine.SwitchGuiPage("page_loading.xml", {
1338                         "attribs": g_GameAttributes,
1339                         "isNetworked" : g_IsNetworked,
1340                         "playerAssignments": g_PlayerAssignments
1341                 });
1342         }
1346  * Don't set any attributes here, just show the changes in the GUI.
1348  * Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously.
1349  */
1350 function updateGUIObjects()
1352         g_IsInGuiUpdate = true;
1354         let mapSettings = g_GameAttributes.settings;
1356         // These dropdowns don't set values while g_IsInGuiUpdate
1357         let mapName = g_GameAttributes.map || "";
1358         let mapFilterIdx = g_MapFilters.findIndex(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "default"));
1359         let mapTypeIdx = g_GameAttributes.mapType !== undefined ? g_MapTypes.Name.indexOf(g_GameAttributes.mapType) : g_MapTypes.Default;
1360         let gameSpeedIdx = g_GameAttributes.gameSpeed !== undefined ? g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) : g_GameSpeeds.Default;
1362         // These dropdowns might set the default (as they ignore g_IsInGuiUpdate)
1363         let mapSizeIdx = mapSettings.Size !== undefined ? g_MapSizes.Tiles.indexOf(mapSettings.Size) : g_MapSizes.Default;
1364         let victoryIdx = mapSettings.GameType !== undefined ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
1365         let wonderDurationIdx = mapSettings.WonderDuration !== undefined ? g_WonderDurations.Duration.indexOf(mapSettings.WonderDuration) : g_WonderDurations.Default;
1366         let popIdx = mapSettings.PopulationCap !== undefined ? g_PopulationCapacities.Population.indexOf(mapSettings.PopulationCap) : g_PopulationCapacities.Default;
1367         let startingResIdx = mapSettings.StartingResources !== undefined ? g_StartingResources.Resources.indexOf(mapSettings.StartingResources) : g_StartingResources.Default;
1368         let ceasefireIdx = mapSettings.Ceasefire !== undefined ? g_Ceasefire.Duration.indexOf(mapSettings.Ceasefire) : g_Ceasefire.Default;
1369         let numPlayers = mapSettings.PlayerData ? mapSettings.PlayerData.length : g_MaxPlayers;
1371         if (g_IsController)
1372         {
1373                 Engine.GetGUIObjectByName("mapType").selected = mapTypeIdx;
1374                 Engine.GetGUIObjectByName("mapFilter").selected = mapFilterIdx;
1375                 Engine.GetGUIObjectByName("mapSelection").selected = Engine.GetGUIObjectByName("mapSelection").list_data.indexOf(mapName);
1376                 Engine.GetGUIObjectByName("mapSize").selected = mapSizeIdx;
1377                 Engine.GetGUIObjectByName("numPlayers").selected = numPlayers - 1;
1378                 Engine.GetGUIObjectByName("victoryCondition").selected = victoryIdx;
1379                 Engine.GetGUIObjectByName("wonderDuration").selected = wonderDurationIdx;
1380                 Engine.GetGUIObjectByName("populationCap").selected = popIdx;
1381                 Engine.GetGUIObjectByName("gameSpeed").selected = gameSpeedIdx;
1382                 Engine.GetGUIObjectByName("ceasefire").selected = ceasefireIdx;
1383                 Engine.GetGUIObjectByName("startingResources").selected = startingResIdx;
1384         }
1385         else
1386         {
1387                 Engine.GetGUIObjectByName("mapTypeText").caption = g_MapTypes.Title[mapTypeIdx];
1388                 Engine.GetGUIObjectByName("mapFilterText").caption = g_MapFilters[mapFilterIdx].name;
1389                 Engine.GetGUIObjectByName("mapSelectionText").caption = mapName == "random" ? g_RandomMap : translate(getMapDisplayName(mapName));
1390                 initMapNameList();
1391         }
1393         // Can be visible to both host and clients
1394         Engine.GetGUIObjectByName("mapSizeText").caption = g_GameAttributes.mapType == "random" ? g_MapSizes.Name[mapSizeIdx] : translate("Default");
1395         Engine.GetGUIObjectByName("numPlayersText").caption = numPlayers;
1396         Engine.GetGUIObjectByName("victoryConditionText").caption = g_VictoryConditions.Title[victoryIdx];
1397         Engine.GetGUIObjectByName("wonderDurationText").caption = g_WonderDurations.Title[wonderDurationIdx];
1398         Engine.GetGUIObjectByName("populationCapText").caption = g_PopulationCapacities.Title[popIdx];
1399         Engine.GetGUIObjectByName("startingResourcesText").caption = g_StartingResources.Title[startingResIdx];
1400         Engine.GetGUIObjectByName("ceasefireText").caption = g_Ceasefire.Title[ceasefireIdx];
1401         Engine.GetGUIObjectByName("gameSpeedText").caption = g_GameSpeeds.Title[gameSpeedIdx];
1403         setGUIBoolean("enableCheats", "enableCheatsText", !!mapSettings.CheatsEnabled);
1404         setGUIBoolean("disableTreasures", "disableTreasuresText", !!mapSettings.DisableTreasures);
1405         setGUIBoolean("exploreMap", "exploreMapText", !!mapSettings.ExploreMap);
1406         setGUIBoolean("revealMap", "revealMapText", !!mapSettings.RevealMap);
1407         setGUIBoolean("lockTeams", "lockTeamsText", !!mapSettings.LockTeams);
1408         setGUIBoolean("lastManStanding", "lastManStandingText", !!mapSettings.LastManStanding);
1409         setGUIBoolean("enableRating", "enableRatingText", !!mapSettings.RatingEnabled);
1411         Engine.GetGUIObjectByName("optionWonderDuration").hidden =
1412                 g_GameAttributes.settings.GameType &&
1413                 g_GameAttributes.settings.GameType != "wonder";
1415         Engine.GetGUIObjectByName("cheatWarningText").hidden = !g_IsNetworked || !mapSettings.CheatsEnabled;
1417         Engine.GetGUIObjectByName("lastManStanding").enabled = !mapSettings.LockTeams;
1418         Engine.GetGUIObjectByName("enableCheats").enabled = !mapSettings.RatingEnabled;
1419         Engine.GetGUIObjectByName("lockTeams").enabled = !mapSettings.RatingEnabled;
1421         // Mapsize completely hidden for non-random maps
1422         let isRandom = g_GameAttributes.mapType == "random";
1423         Engine.GetGUIObjectByName("mapSizeDesc").hidden = !isRandom;
1424         Engine.GetGUIObjectByName("mapSize").hidden = !isRandom || !g_IsController;
1425         Engine.GetGUIObjectByName("mapSizeText").hidden = !isRandom || g_IsController;
1426         hideControl("numPlayers", "numPlayersText", isRandom && g_IsController);
1428         let notScenario = g_GameAttributes.mapType != "scenario" && g_IsController ;
1430         for (let ctrl of ["victoryCondition", "wonderDuration", "populationCap",
1431                           "startingResources", "ceasefire", "revealMap",
1432                           "exploreMap", "disableTreasures", "lockTeams", "lastManStanding"])
1433                 hideControl(ctrl, ctrl + "Text", notScenario);
1435         Engine.GetGUIObjectByName("civResetButton").hidden = !notScenario;
1436         Engine.GetGUIObjectByName("teamResetButton").hidden = !notScenario;
1438         for (let i = 0; i < g_MaxPlayers; ++i)
1439         {
1440                 Engine.GetGUIObjectByName("playerBox["+i+"]").hidden = (i >= numPlayers);
1442                 if (i >= numPlayers)
1443                         continue;
1445                 let pName = Engine.GetGUIObjectByName("playerName["+i+"]");
1446                 let pAssignment = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
1447                 let pAssignmentText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
1448                 let pCiv = Engine.GetGUIObjectByName("playerCiv["+i+"]");
1449                 let pCivText = Engine.GetGUIObjectByName("playerCivText["+i+"]");
1450                 let pTeam = Engine.GetGUIObjectByName("playerTeam["+i+"]");
1451                 let pTeamText = Engine.GetGUIObjectByName("playerTeamText["+i+"]");
1452                 let pColor = Engine.GetGUIObjectByName("playerColor["+i+"]");
1454                 let pData = mapSettings.PlayerData ? mapSettings.PlayerData[i] : {};
1455                 let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[i] : {};
1457                 let color = getSetting(pData, pDefs, "Color");
1458                 pColor.sprite = "color:" + rgbToGuiColor(color) + " 100";
1459                 pName.caption = translate(getSetting(pData, pDefs, "Name"));
1461                 let team = getSetting(pData, pDefs, "Team");
1462                 let civ = getSetting(pData, pDefs, "Civ");
1464                 pAssignmentText.caption = pAssignment.list[0] ? pAssignment.list[Math.max(0, pAssignment.selected)] : translate("Loading...");
1465                 pCivText.caption = civ == "random" ? g_RandomCiv : (g_CivData[civ] ? g_CivData[civ].Name : "Unknown");
1466                 pTeamText.caption = (team !== undefined && team >= 0) ? team+1 : "-";
1468                 pCiv.selected = civ ? pCiv.list_data.indexOf(civ) : 0;
1469                 pTeam.selected = team !== undefined && team >= 0 ? team+1 : 0;
1471                 hideControl("playerAssignment["+i+"]", "playerAssignmentText["+i+"]", g_IsController);
1472                 hideControl("playerCiv["+i+"]", "playerCivText["+i+"]", notScenario);
1473                 hideControl("playerTeam["+i+"]", "playerTeamText["+i+"]", notScenario);
1475                 // Allow host to chose player colors on non-scenario maps
1476                 let pColorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
1477                 let pColorPickerHeading = Engine.GetGUIObjectByName("playerColorHeading");
1478                 let canChangeColors = g_IsController && g_GameAttributes.mapType != "scenario";
1479                 pColorPicker.hidden = !canChangeColors;
1480                 pColorPickerHeading.hidden = !canChangeColors;
1481                 if (canChangeColors)
1482                         pColorPicker.selected = g_PlayerColors.findIndex(col => sameColor(col, color));
1483         }
1485         updateGameDescription();
1486         resizeMoreOptionsWindow();
1488         g_IsInGuiUpdate = false;
1490         // Game attributes include AI settings, so update the player list
1491         updatePlayerList();
1493         resetReadyData();
1495         // Refresh AI config page
1496         if (g_LastViewedAIPlayer != -1)
1497         {
1498                 Engine.PopGuiPage();
1499                 openAIConfig(g_LastViewedAIPlayer);
1500         }
1503 function updateGameDescription()
1505         setMapPreviewImage("mapPreview", getMapPreview(g_GameAttributes.map));
1507         Engine.GetGUIObjectByName("mapInfoName").caption =
1508                 translateMapTitle(getMapDisplayName(g_GameAttributes.map));
1510         Engine.GetGUIObjectByName("mapInfoDescription").caption = getGameDescription();
1514  * Broadcast the changed settings to all clients and the lobbybot.
1515  */
1516 function updateGameAttributes()
1518         if (g_IsInGuiUpdate || !g_IsController)
1519                 return;
1521         if (g_IsNetworked)
1522         {
1523                 Engine.SetNetworkGameAttributes(g_GameAttributes);
1524                 if (g_LoadingState >= 2)
1525                         sendRegisterGameStanza();
1526         }
1527         else
1528                 updateGUIObjects();
1531 function openAIConfig(playerSlot)
1533         g_LastViewedAIPlayer = playerSlot;
1535         Engine.PushGuiPage("page_aiconfig.xml", {
1536                 "callback": "AIConfigCallback",
1537                 "isController": g_IsController,
1538                 "playerSlot": playerSlot,
1539                 "id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
1540                 "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff
1541         });
1545  * Called after closing the dialog.
1546  */
1547 function AIConfigCallback(ai)
1549         g_LastViewedAIPlayer = -1;
1551         if (!ai.save || !g_IsController)
1552                 return;
1554         g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
1555         g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
1557         updateGameAttributes();
1560 function updatePlayerList()
1562         g_IsInGuiUpdate = true;
1564         let hostNameList = [];
1565         let hostGuidList = [];
1566         let assignments = [];
1567         let aiAssignments = {};
1568         let noAssignment;
1569         let assignedCount = 0;
1570         for (let guid of sortGUIDsByPlayerID())
1571         {
1572                 let player = g_PlayerAssignments[guid].player;
1574                 if (player != -1)
1575                         hostNameList.push(g_PlayerAssignments[guid].name);
1576                 else
1577                         hostNameList.push("[color=\""+ g_UnassignedPlayerColor + "\"]" + g_PlayerAssignments[guid].name + "[/color]");
1579                 hostGuidList.push(guid);
1580                 assignments[player] = hostNameList.length-1;
1582                 if (player != -1)
1583                         ++assignedCount;
1584         }
1586         // Only enable start button if we have enough assigned players
1587         if (g_IsController)
1588                 Engine.GetGUIObjectByName("startGame").enabled = assignedCount > 0;
1590         for (let ai of g_Settings.AIDescriptions)
1591         {
1592                 // If the map uses a hidden AI then don't hide it
1593                 if (ai.data.hidden && g_GameAttributes.settings.PlayerData.every(pData => pData.AI != ai.id))
1594                         continue;
1596                 aiAssignments[ai.id] = hostNameList.length;
1597                 hostNameList.push("[color=\""+ g_AIColor + "\"]" + sprintf(translate("AI: %(ai)s"), { "ai": translate(ai.data.name) }));
1598                 hostGuidList.push("ai:" + ai.id);
1599         }
1601         noAssignment = hostNameList.length;
1602         hostNameList.push("[color=\""+ g_UnassignedColor + "\"]" + translate("Unassigned"));
1603         hostGuidList.push("");
1605         for (let i = 0; i < g_MaxPlayers; ++i)
1606         {
1607                 let playerSlot = i;
1608                 let playerID = i+1; // we don't show Gaia, so first slot is ID 1
1610                 let selection = assignments[playerID];
1612                 let configButton = Engine.GetGUIObjectByName("playerConfig["+i+"]");
1613                 configButton.hidden = true;
1615                 // Look for valid player slots
1616                 if (playerSlot >= g_GameAttributes.settings.PlayerData.length)
1617                         continue;
1619                 // If no human is assigned, look for an AI instead
1620                 if (selection === undefined)
1621                 {
1622                         let aiId = g_GameAttributes.settings.PlayerData[playerSlot].AI;
1623                         if (aiId)
1624                         {
1625                                 // Check for a valid AI
1626                                 if (aiId in aiAssignments)
1627                                 {
1628                                         selection = aiAssignments[aiId];
1629                                         configButton.hidden = false;
1630                                         configButton.onpress = function()
1631                                         {
1632                                                 openAIConfig(playerSlot);
1633                                         };
1634                                 }
1635                                 else
1636                                 {
1637                                         g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
1638                                         warn("AI \"" + aiId + "\" not present. Defaulting to unassigned.");
1639                                 }
1640                         }
1642                         if (!selection)
1643                                 selection = noAssignment;
1644                 }
1645                 // There was a human, so make sure we don't have any AI left
1646                 // over in their slot, if we're in charge of the attributes
1647                 else if (g_IsController && g_GameAttributes.settings.PlayerData[playerSlot].AI)
1648                 {
1649                         g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
1650                         if (g_IsNetworked)
1651                                 Engine.SetNetworkGameAttributes(g_GameAttributes);
1652                 }
1654                 let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
1655                 let assignBoxText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
1656                 assignBox.list = hostNameList;
1657                 assignBox.list_data = hostGuidList;
1658                 if (assignBox.selected != selection)
1659                         assignBox.selected = selection;
1660                 assignBoxText.caption = hostNameList[selection];
1662                 if (g_IsController)
1663                         assignBox.onselectionchange = function() {
1664                                 if (g_IsInGuiUpdate)
1665                                         return;
1667                                 let guid = hostGuidList[this.selected];
1668                                 if (!guid)
1669                                 {
1670                                         if (g_IsNetworked)
1671                                                 // Unassign any host from this player slot
1672                                                 Engine.AssignNetworkPlayer(playerID, "");
1673                                         // Remove AI from this player slot
1674                                         g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
1675                                 }
1676                                 else if (guid.substr(0, 3) == "ai:")
1677                                 {
1678                                         if (g_IsNetworked)
1679                                                 // Unassign any host from this player slot
1680                                                 Engine.AssignNetworkPlayer(playerID, "");
1681                                         // Set the AI for this player slot
1682                                         g_GameAttributes.settings.PlayerData[playerSlot].AI = guid.substr(3);
1683                                 }
1684                                 else
1685                                         swapPlayers(guid, playerSlot);
1687                                 if (g_IsNetworked)
1688                                         Engine.SetNetworkGameAttributes(g_GameAttributes);
1689                                 else
1690                                         updatePlayerList();
1691                                 updateReadyUI();
1692                         };
1693         }
1695         g_IsInGuiUpdate = false;
1698 function swapPlayers(guid, newSlot)
1700         // Player slots are indexed from 0 as Gaia is omitted.
1701         let newPlayerID = newSlot + 1;
1702         let playerID = g_PlayerAssignments[guid].player;
1704         // Attempt to swap the player or AI occupying the target slot,
1705         // if any, into the slot this player is currently in.
1706         if (playerID != -1)
1707         {
1708                 for (let guid in g_PlayerAssignments)
1709                 {
1710                         // Move the player in the destination slot into the current slot.
1711                         if (g_PlayerAssignments[guid].player != newPlayerID)
1712                                 continue;
1714                         if (g_IsNetworked)
1715                                 Engine.AssignNetworkPlayer(playerID, guid);
1716                         else
1717                                 g_PlayerAssignments[guid].player = playerID;
1718                         break;
1719                 }
1721                 // Transfer the AI from the target slot to the current slot.
1722                 g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI;
1724                 // Swap civilizations if they aren't fixed
1725                 if (g_GameAttributes.mapType != "scenario")
1726                 {
1727                         [g_GameAttributes.settings.PlayerData[playerID - 1].Civ, g_GameAttributes.settings.PlayerData[newSlot].Civ] =
1728                                 [g_GameAttributes.settings.PlayerData[newSlot].Civ, g_GameAttributes.settings.PlayerData[playerID - 1].Civ];
1729                 }
1730         }
1732         if (g_IsNetworked)
1733                 Engine.AssignNetworkPlayer(newPlayerID, guid);
1734         else
1735                 g_PlayerAssignments[guid].player = newPlayerID;
1737         g_GameAttributes.settings.PlayerData[newSlot].AI = "";
1740 function submitChatInput()
1742         let input = Engine.GetGUIObjectByName("chatInput");
1743         let text = input.caption;
1744         if (!text.length)
1745                 return;
1747         input.caption = "";
1749         if (executeNetworkCommand(text))
1750                 return;
1752         Engine.SendNetworkChat(text);
1755 function senderFont(text)
1757         return '[font="' + g_SenderFont + '"]' + text + '[/font]';
1760 function systemMessage(message)
1762         return senderFont(sprintf(translate("== %(message)s"), { "message": message }));
1765 function colorizePlayernameByGUID(guid, username = "")
1767         // TODO: Maybe the host should have the moderator-prefix?
1768         if (!username)
1769                 username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player");
1770         let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
1772         let color = "white";
1773         if (playerID > 0)
1774         {
1775                 color = g_GameAttributes.settings.PlayerData[playerID - 1].Color;
1777                 // Enlighten playercolor to improve readability
1778                 let [h, s, l] = rgbToHsl(color.r, color.g, color.b);
1779                 let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l));
1781                 color = rgbToGuiColor({ "r": r, "g": g, "b": b });
1782         }
1784         return '[color="'+ color +'"]' + username + '[/color]';
1787 function addChatMessage(msg)
1789         if (msg.type != "system" && msg.text)
1790         {
1791                 let userName = g_PlayerAssignments[Engine.GetPlayerGUID() || "local"].name;
1793                 if (userName != g_PlayerAssignments[msg.guid].name)
1794                         notifyUser(userName, msg.text);
1795         }
1797         if (!g_FormatChatMessage[msg.type])
1798                 return;
1800         let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || "");
1802         let text = g_FormatChatMessage[msg.type](msg, user);
1804         if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true")
1805                 text = sprintf(translate("%(time)s %(message)s"), {
1806                         "time": sprintf(translate("\\[%(time)s]"), {
1807                                 "time": Engine.FormatMillisecondsIntoDateString(new Date().getTime(), translate("HH:mm"))
1808                         }),
1809                         "message": text
1810                 });
1812         g_ChatMessages.push(text);
1814         Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
1817 function showMoreOptions(show)
1819         Engine.GetGUIObjectByName("moreOptionsFade").hidden = !show;
1820         Engine.GetGUIObjectByName("moreOptions").hidden = !show;
1823 function resetCivilizations()
1825         for (let i in g_GameAttributes.settings.PlayerData)
1826                 g_GameAttributes.settings.PlayerData[i].Civ = "random";
1828         updateGameAttributes();
1831 function resetTeams()
1833         for (let i in g_GameAttributes.settings.PlayerData)
1834                 g_GameAttributes.settings.PlayerData[i].Team = -1;
1836         updateGameAttributes();
1839 function toggleReady()
1841         setReady(!g_IsReady);
1844 function setReady(ready, sendMessage = true)
1846         g_IsReady = ready;
1848         if (sendMessage)
1849                 Engine.SendNetworkReady(+g_IsReady);
1851         if (g_IsController)
1852                 return;
1854         let button = Engine.GetGUIObjectByName("startGame");
1856         button.caption = g_IsReady ?
1857                 translate("I'm not ready!") :
1858                 translate("I'm ready");
1860         button.tooltip = g_IsReady ?
1861                 translate("State that you are not ready to play.") :
1862                 translate("State that you are ready to play!");
1865 function updateReadyUI()
1867         if (!g_IsNetworked)
1868                 return;
1870         let isAI = new Array(g_MaxPlayers + 1).fill(true);
1871         let allReady = true;
1872         for (let guid in g_PlayerAssignments)
1873         {
1874                 // We don't really care whether observers are ready.
1875                 if (g_PlayerAssignments[guid].player == -1 || !g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
1876                         continue;
1877                 let pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1] : {};
1878                 let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[g_PlayerAssignments[guid].player - 1] : {};
1879                 isAI[g_PlayerAssignments[guid].player] = false;
1880                 if (g_PlayerAssignments[guid].status || !g_IsNetworked)
1881                         Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = '[color="' + g_ReadyColor + '"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]';
1882                 else
1883                 {
1884                         Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = translate(getSetting(pData, pDefs, "Name"));
1885                         allReady = false;
1886                 }
1887         }
1889         // AIs are always ready.
1890         for (let playerid = 0; playerid < g_MaxPlayers; ++playerid)
1891         {
1892                 if (!g_GameAttributes.settings.PlayerData[playerid])
1893                         continue;
1894                 let pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[playerid] : {};
1895                 let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[playerid] : {};
1896                 if (isAI[playerid + 1])
1897                         Engine.GetGUIObjectByName("playerName[" + playerid + "]").caption = '[color="' + g_ReadyColor + '"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]';
1898         }
1900         // The host is not allowed to start until everyone is ready.
1901         if (g_IsNetworked && g_IsController)
1902         {
1903                 let startGameButton = Engine.GetGUIObjectByName("startGame");
1904                 startGameButton.enabled = allReady;
1905                 // Add a explanation on to the tooltip if disabled.
1906                 let disabledIndex = startGameButton.tooltip.indexOf('Disabled');
1907                 if (disabledIndex != -1 && allReady)
1908                         startGameButton.tooltip = startGameButton.tooltip.substring(0, disabledIndex - 2);
1909                 else if (disabledIndex == -1 && !allReady)
1910                         startGameButton.tooltip = startGameButton.tooltip + " (Disabled until all players are ready)";
1911         }
1914 function resetReadyData()
1916         if (g_GameStarted)
1917                 return;
1919         if (g_ReadyChanged < 1)
1920                 addChatMessage({ "type": "settings" });
1921         else if (g_ReadyChanged == 2 && !g_ReadyInit)
1922                 return; // duplicate calls on init
1923         else
1924                 g_ReadyInit = false;
1926         g_ReadyChanged = 2;
1927         if (!g_IsNetworked)
1928                 g_IsReady = true;
1929         else if (g_IsController)
1930         {
1931                 Engine.ClearAllPlayerReady();
1932                 setReady(true);
1933         }
1934         else
1935                 setReady(false, false);
1939  * Send a list of playernames and distinct between players and observers.
1940  * Don't send teams, AIs or anything else until the game was started.
1941  * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
1942  */
1943 function formatClientsForStanza()
1945         let connectedPlayers = 0;
1946         let playerData = [];
1948         for (let guid in g_PlayerAssignments)
1949         {
1950                 let pData = { "Name": g_PlayerAssignments[guid].name };
1952                 if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
1953                         ++connectedPlayers;
1954                 else
1955                         pData.Team = "observer";
1957                 playerData.push(pData);
1958         }
1960         return {
1961                 "list": playerDataToStringifiedTeamList(playerData),
1962                 "connectedPlayers": connectedPlayers
1963         };
1967  * Send the relevant gamesettings to the lobbybot.
1968  */
1969 function sendRegisterGameStanza()
1971         if (!g_IsController || !Engine.HasXmppClient())
1972                 return;
1974         let selectedMapSize = Engine.GetGUIObjectByName("mapSize").selected;
1975         let selectedVictoryCondition = Engine.GetGUIObjectByName("victoryCondition").selected;
1977         let mapSize = g_GameAttributes.mapType == "random" ? Engine.GetGUIObjectByName("mapSize").list_data[selectedMapSize] : "Default";
1978         let victoryCondition = Engine.GetGUIObjectByName("victoryCondition").list[selectedVictoryCondition];
1979         let clients = formatClientsForStanza();
1981         let stanza = {
1982                 "name": g_ServerName,
1983                 "port": g_ServerPort,
1984                 "mapName": g_GameAttributes.map,
1985                 "niceMapName": getMapDisplayName(g_GameAttributes.map),
1986                 "mapSize": mapSize,
1987                 "mapType": g_GameAttributes.mapType,
1988                 "victoryCondition": victoryCondition,
1989                 "nbp": clients.connectedPlayers,
1990                 "maxnbp": g_GameAttributes.settings.PlayerData.length,
1991                 "players": clients.list,
1992         };
1994         // Only send the stanza if the relevant settings actually changed
1995         if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop]))
1996                 return;
1998         g_LastGameStanza = stanza;
1999         Engine.SendRegisterGame(stanza);