Ticket #3994: t3994_gamesetup_GUI_rewrite_v3.patch

File t3994_gamesetup_GUI_rewrite_v3.patch, 54.7 KB (added by elexis, 8 years ago)

Committed some parts of the previous patch in r18315, r18299, r18298, r18297 and r18296. Rebased the patch after like 10 hunks with hundreds loc failed. g_DropdownArrays is half implemented half broken.

  • binaries/data/mods/public/gui/gamesetup/gamesetup.js

     
    11const g_MatchSettings_SP = "config/matchsettings.json";
    22const g_MatchSettings_MP = "config/matchsettings.mp.json";
    33
     4const g_PlayerArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ..., MaxPlayers
    45const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
    56const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly));
    67const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
    78const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
    89const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
    910const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
    1011const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions);
    1112const g_WonderDurations = prepareForDropdown(g_Settings && g_Settings.WonderDurations);
    1213
    1314/**
     15 * Highlight the "random" dropdownlist item.
     16 */
     17const g_ColorRandom = "orange";
     18
     19const g_TeamsArray = prepareForDropdown([{
     20        "label": translateWithContext("team", "None"),
     21        "id": -1
     22    }].concat(
     23        Array(g_MaxTeams).fill(0).map((v, i) => ({
     24            "label": i + 1,
     25            "id": i
     26        }))
     27    )
     28);
     29
     30/**
     31 * Offer users to select playable civs only.
     32 * Load unselectable civs as they could appear in scenario maps.
     33 */
     34const g_CivData = loadCivData();
     35
     36const g_CivList = g_CivData && prepareForDropdown([{
     37        "name": '[color="' + g_ColorRandom + '"]' + translateWithContext("civilization", "Random") + '[/color]',
     38        "code": "random"
     39    }].concat(
     40        Object.keys(g_CivData).filter(
     41            civ => g_CivData[civ].SelectableInGameSetup
     42        ).map(civ => ({
     43            "name": g_CivData[civ].Name,
     44            "code": civ
     45        })).sort(sortNameIgnoreCase)
     46    )
     47);
     48
     49/**
    1450 * All selectable playercolors except gaia.
    1551 */
    1652const g_PlayerColors = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
    1753
    1854/**
    const g_MapPath = {  
    2561};
    2662
    2763/**
    2864 * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
    2965 */
    30 const g_NetMessageTypes = {
     66var g_NetMessageTypes = {
    3167    "netstatus": msg => handleNetStatusMessage(msg),
    3268    "netwarn": msg => addNetworkWarning(msg),
    3369    "gamesetup": msg => handleGamesetupMessage(msg),
    3470    "players": msg => handlePlayerAssignmentMessage(msg),
    3571    "ready": msg => handleReadyMessage(msg),
    const g_NetMessageTypes = {  
    3773    "kicked": msg => addChatMessage({ "type": "kicked", "username": msg.username }),
    3874    "banned": msg => addChatMessage({ "type": "banned", "username": msg.username }),
    3975    "chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text })
    4076};
    4177
    42 const g_FormatChatMessage = {
     78var g_FormatChatMessage = {
    4379    "system": (msg, user) => systemMessage(msg.text),
    4480    "settings": (msg, user) => systemMessage(translate('Game settings have been changed')),
    4581    "connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })),
    4682    "disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })),
    4783    "kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })),
    const g_FormatChatMessage = {  
    5793        "username": user
    5894    }),
    5995    "clientlist": (msg, user) => getUsernameList()
    6096};
    6197
    62 /**
    63  * The dropdownlist items will appear in the order they are added.
    64  */
    65 const g_MapFilters = [
     98var g_MapFilters = prepareForDropdown([
    6699    {
    67100        "id": "default",
    68101        "name": translate("Default"),
    69         "filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1)
     102        "filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1),
     103        "Default": true
    70104    },
    71105    {
    72106        "id": "naval",
    73107        "name": translate("Naval Maps"),
    74108        "filter": mapKeywords => mapKeywords.indexOf("naval") != -1
    const g_MapFilters = [  
    86120    {
    87121        "id": "all",
    88122        "name": translate("All Maps"),
    89123        "filter": mapKeywords => true
    90124    }
    91 ];
     125]);
    92126
    93127/**
    94128 * Used for generating the botnames.
    95129 */
    96130const g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
    97131
    98132/**
    99  * Offer users to select playable civs only.
    100  * Load unselectable civs as they could appear in scenario maps.
    101  */
    102 const g_CivData = loadCivData();
    103 
    104 /**
    105133 * Used for highlighting the sender of chat messages.
    106134 */
    107135const g_SenderFont = "sans-bold-13";
    108136
    109137/**
    110  * Highlight the "random" dropdownlist item.
    111  */
    112 const g_ColorRandom = "orange";
    113 
    114 /**
    115138 * Highlight AIs in the player-dropdownlist.
    116139 */
    117140const g_AIColor = "70 150 70";
    118141
    119142/**
    const g_ReadyColor = "green";  
    135158 * Highlights the victory condition in the game-description.
    136159 */
    137160const g_VictoryColor = "orange";
    138161
    139162/**
    140  * Placeholder item for the map-dropdownlist.
    141  */
    142 const g_RandomMap = '[color="' + g_ColorRandom + '"]' + translateWithContext("map type", "Random") + "[/color]";
    143 
    144 /**
    145  * Placeholder item for the civ-dropdownlists.
    146  */
    147 const g_RandomCiv = '[color="' + g_ColorRandom + '"]' + translateWithContext("civilization", "Random") + '[/color]';
    148 
    149 /**
    150163 * Whether this is a single- or multiplayer match.
    151164 */
    152165var g_IsNetworked;
    153166
    154167/**
    var g_DefaultPlayerData = [];  
    198211var g_GameAttributes = { "settings": {} };
    199212
    200213var g_ChatMessages = [];
    201214
    202215/**
    203  * Cache containing the mapsettings for scenario/skirmish maps. Just-in-time loading.
     216 * Filename and translated title of all maps, given the currently selected
     217 * maptype and filter. Sorted by title, shown in the dropdown.
     218 */
     219var g_MapList = [];
     220
     221/**
     222 * Cache containing the mapsettings. Just-in-time loading.
    204223 */
    205224var g_MapData = {};
    206225
    207226/**
    208227 * Wait one tick before initializing the GUI objects and
    var g_LastGameStanza;  
    217236
    218237/**
    219238 * Remembers if the current player viewed the AI settings of some playerslot.
    220239 */
    221240var g_LastViewedAIPlayer = -1;
     241/**
     242 * Contains the logic of all multiple-choice gamesettings.
     243 *
     244 * Hidden - if so, both label and dropdown won't be visible.
     245 * Enabled - Only the label will be shown if it's disabled.
     246 * Default - returns the index of the default value (not the value itself).
     247 *
     248 * NOTICE: The first three elements need to be initialized first.
     249 * If the map is changed, missing values are supplemented with defaults.
     250 */
     251var g_Dropdowns = g_Settings && {
     252    "mapType": {
     253        "labels": () => g_MapTypes.Title,
     254        "ids": () => g_MapTypes.Name,
     255        "default": () => g_MapTypes.Default,
     256        "defined": () => g_GameAttributes.mapType !== undefined,
     257        "get": () => g_GameAttributes.mapType,
     258        "select": idx => {
     259
     260            g_MapData = {};
     261
     262            g_GameAttributes.mapType = g_MapTypes.Name[idx];
     263            g_GameAttributes.mapPath = g_MapPath[g_GameAttributes.mapType];
     264            delete g_GameAttributes.map;
     265
     266            if (g_GameAttributes.mapType != "scenario")
     267                g_GameAttributes.settings = {
     268                    "PlayerData": g_DefaultPlayerData.slice(0, 4)
     269                };
     270
     271            reloadMapList();
     272            supplementDefaults();
     273        }
     274    },
     275    "mapFilter": {
     276        "labels": () => g_MapFilters.name,
     277        "ids": () => g_MapFilters.id,
     278        "default": () => g_MapFilters.Default,
     279        "defined": () => g_GameAttributes.mapFilter !== undefined,
     280        "get": () => g_GameAttributes.mapFilter,
     281        "select": idx => {
     282            g_GameAttributes.mapFilter = g_MapFilters.id[idx];
     283            delete g_GameAttributes.map;
     284            reloadMapList();
     285            supplementDefaults();
     286        }
     287    },
     288    "mapSelection": {
     289        "labels": () => g_MapList.name,
     290        "ids": () => g_MapList.file,
     291        "default": () => 0,
     292        "defined": () => g_GameAttributes.map !== undefined,
     293        "get": () => g_GameAttributes.map,
     294        "select": idx => {
     295            selectMap(g_MapList.file[idx]);
     296            supplementDefaults();
     297        }
     298    },
     299    "mapSize": {
     300        "labels": () => g_MapSizes.LongName,
     301        "ids": () => g_MapSizes.Tiles,
     302        "default": () => g_MapSizes.Default,
     303        "defined": () => g_GameAttributes.settings.Size !== undefined,
     304        "get": () => g_GameAttributes.settings.Size,
     305        "select": idx => {
     306            g_GameAttributes.settings.Size = g_MapSizes.Tiles[idx];
     307        },
     308        "maps": ["random"]
     309    },
     310    "numPlayers": {
     311        "labels": () => g_PlayerArray,
     312        "ids": () => g_PlayerArray,
     313        "default": () => g_MaxPlayers - 1,
     314        "defined": () => g_GameAttributes.settings.PlayerData !== undefined,
     315        "get": () => g_GameAttributes.settings.PlayerData.length,
     316        "select": idx => {
     317            selectNumPlayers(idx + 1);
     318        },
     319        "maps": ["random"]
     320    },
     321    "populationCap": {
     322        "labels": () => g_PopulationCapacities.Title,
     323        "ids": () => g_PopulationCapacities.Population,
     324        "default": () => g_PopulationCapacities.Default,
     325        "defined": () => g_GameAttributes.settings.PopulationCap !== undefined,
     326        "get": () => g_GameAttributes.settings.PopulationCap,
     327        "select": idx => {
     328            g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[idx];
     329        },
     330        "maps": ["random", "skirmish"]
     331    },
     332    "startingResources": {
     333        "labels": () => g_StartingResources.Title,
     334        "ids": () => g_StartingResources.Resources,
     335        "default": () => g_StartingResources.Default,
     336        "defined": () => g_GameAttributes.settings.StartingResources !== undefined,
     337        "get": () => g_GameAttributes.settings.StartingResources,
     338        "select": idx => {
     339            g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[idx];
     340        },
     341        "maps": ["random", "skirmish"]
     342    },
     343    "ceasefire": {
     344        "labels": () => g_Ceasefire.Title,
     345        "ids": () => g_Ceasefire.Duration,
     346        "default": () => g_Ceasefire.Default,
     347        "defined": () => g_GameAttributes.settings.Ceasefire !== undefined,
     348        "get": () => g_GameAttributes.settings.Ceasefire,
     349        "select": idx => {
     350            // TODO for (let ai of )
     351            g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[idx];
     352        },
     353        "maps": ["random", "skirmish"]
     354    },
     355    "victoryCondition": {
     356        "labels": () => g_VictoryConditions.Title,
     357        "ids": () => g_VictoryConditions.Name,
     358        "default": () => g_VictoryConditions.Default,
     359        "defined": () => g_GameAttributes.settings.GameType !== undefined,
     360        "get": () => g_GameAttributes.settings.GameType,
     361        "select": idx => {
     362            g_GameAttributes.settings.GameType = g_VictoryConditions.Name[idx];
     363            g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[idx];
     364        },
     365        "maps": ["random", "skirmish"]
     366    },
     367    "wonderDuration": {
     368        "labels": () => g_WonderDurations.Title,
     369        "ids": () => g_WonderDurations.Duration,
     370        "default": () => g_WonderDurations.Default,
     371        "defined": () => g_GameAttributes.settings.WonderDuration !== undefined,
     372        "get": () => g_GameAttributes.settings.WonderDuration,
     373        "select": idx => {
     374            g_GameAttributes.settings.WonderDuration = g_WonderDurations.Duration[idx];
     375        },
     376        "maps": ["random", "skirmish"]
     377    },
     378    "gameSpeed": {
     379        "labels": () => g_GameSpeeds.Title,
     380        "ids": () => g_GameSpeeds.Speed,
     381        "default": () => g_GameSpeeds.Default,
     382        "defined": () => g_GameAttributes.gameSpeed !== undefined,
     383        "get": () => g_GameAttributes.gameSpeed,
     384        "select": idx => {
     385            g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[idx];
     386        }
     387    }
     388};
     389var g_HostNameList = [];
     390var g_HostGUIDList = [];
     391
     392var g_DropdownArrays = {
     393    "playerAssignment": {
     394        "labels": (idx) => g_HostNameList,
     395        "ids": (idx) => g_HostGUIDList,
     396        "default": (idx) => 0, // TODO: AI player
     397        "defined": (idx) => true,
     398        "get": (idx) => 0,// TODO
     399        "select": (idx, selectedIdx) => {
     400
     401            // TODO: text had shown translate("Loading...") on init is that relevant?
     402            let guid = g_HostGUIDList[selectedIdx];
     403            if (!guid || guid.substr(0, 3) == "ai:")
     404            {
     405                if (g_IsNetworked)
     406                    Engine.AssignNetworkPlayer(playerID, "");
     407
     408                g_GameAttributes.settings.PlayerData[idx].AI = guid ? guid.substr(3) : "";
     409            }
     410            else
     411                swapPlayers(guid, idx);
     412
     413            updateGameAttributes();
     414            updateReadyUI();
     415        }
     416    },
     417    "playerTeam": {
     418        "labels": (idx) => g_TeamsArray.label,
     419        "ids": (idx) => g_TeamsArray.id,
     420        "default": (idx) => 0,
     421        "defined": (idx) => g_GameAttributes.settings.PlayerData[idx].Team !== undefined,
     422        "get": (idx) => warn(idx),//g_GameAttributes.settings.PlayerData[idx].Team,
     423        "select": (idx, selectedIdx) => {
     424            g_GameAttributes.settings.PlayerData[idx].Team = selectedIdx - 1;
     425        },
     426        "maps": ["random", "skirmish"]
     427    },
     428    "playerCiv": {
     429        "labels": (idx) => g_CivList.name,
     430        "ids": (idx) => g_CivList.code,
     431        "default": (idx) => 0,
     432        "defined": (idx) => g_GameAttributes.settings.PlayerData[idx].Civ !== undefined,
     433        "get": (idx) => g_GameAttributes.settings.PlayerData[idx].Civ,
     434        "select": (idx, selectedIdx) => {
     435            g_GameAttributes.settings.PlayerData[idx].Civ = g_CivList.code[selectedIdx];
     436        },
     437        "maps": ["random", "skirmish"]
     438    },
     439    "playerColorPicker": {
     440        "labels": (idx) => g_PlayerColors.map(color => ' ' + '[color="' + rgbToGuiColor(color) + '"]■[/color]'),
     441        "ids": (idx) => g_PlayerColors.map((color, index) => index),
     442        "default": (idx) => idx,
     443        "defined": (idx) => g_GameAttributes.settings.PlayerData[idx].Color !== undefined,
     444        "get": (idx) => g_GameAttributes.settings.PlayerData[idx].Color,
     445        "select": (idx, selectedIdx) => {
     446            let playerData = g_GameAttributes.settings.PlayerData;
     447
     448            // If someone else has that color, give that player the old color
     449            let pData = playerData.find(pData => sameColor(g_PlayerColors[selectedIdx], pData.Color));
     450            if (pData)
     451                pData.Color = playerData[idx].Color;
     452
     453            playerData[idx].Color = g_PlayerColors[selectedIdx];
     454            ensureUniquePlayerColors(playerData);
     455        },
     456        "maps": ["random", "skirmish"]
     457    }
     458};
     459
     460/**
     461 * Contains the logic of all boolean gamesettings.
     462 */
     463var g_Checkboxes = {
     464    "revealMap": {
     465        "default": () => false,
     466        "defined": () => g_GameAttributes.settings.RevealMap !== undefined,
     467        "get": () => g_GameAttributes.settings.RevealMap,
     468        "set": checked => {
     469            g_GameAttributes.settings.RevealMap = checked;
     470        },
     471        "maps": ["random", "skirmish"]
     472    },
     473    "exploreMap": {
     474        "default": () => false,
     475        "defined": () => g_GameAttributes.settings.ExploreMap !== undefined,
     476        "get": () => g_GameAttributes.settings.ExploreMap,
     477        "set": checked => {
     478            g_GameAttributes.settings.ExploreMap = checked;
     479        },
     480        "maps": ["random", "skirmish"]
     481    },
     482    "disableTreasures": {
     483        "default": () => false,
     484        "defined": () => g_GameAttributes.settings.DisableTreasures !== undefined,
     485        "get": () => g_GameAttributes.settings.DisableTreasures,
     486        "set": checked => {
     487            g_GameAttributes.settings.DisableTreasures = checked;
     488        },
     489        "maps": ["random", "skirmish"]
     490    },
     491    "lockTeams":  {
     492        "default": () => Engine.HasXmppClient(),
     493        "defined": () => g_GameAttributes.settings.LockTeams !== undefined,
     494        "get": () => g_GameAttributes.settings.LockTeams,
     495        "set": checked => {
     496            g_GameAttributes.settings.LockTeams = checked;
     497        },
     498        "maps": ["random", "skirmish"],
     499        "enabled": () => !g_GameAttributes.settings.RatingEnabled
     500    },
     501    "enableCheats":  {
     502        "default": () => !g_IsNetworked,
     503        "hidden": () => !g_IsNetworked,
     504        "defined": () => g_GameAttributes.settings.CheatsEnabled !== undefined,
     505        "get": () => g_GameAttributes.settings.CheatsEnabled,
     506        "set": checked => {
     507            g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked ||
     508                checked && !g_GameAttributes.settings.RatingEnabled;
     509        },
     510        "enabled": () => !g_GameAttributes.settings.RatingEnabled
     511    },
     512    "enableRating":  {
     513        "default": () => Engine.HasXmppClient(),
     514        "defined": () => g_GameAttributes.settings.RatingEnabled !== undefined,
     515        "get": () => !!g_GameAttributes.settings.RatingEnabled,
     516        "set": checked => {
     517            g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient() ? checked : undefined;
     518            Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
     519        }
     520    }
     521};
     522
     523/**
     524 * For setting up some additional GUI objects.
     525 */
     526var g_MiscControls = {
     527    "chatPanel": {
     528        "hidden": () => !g_IsNetworked
     529    },
     530    "optionCheats": {
     531        "hidden": () => !g_IsNetworked
     532    },
     533    "optionRating": {
     534        "hidden": () => !Engine.HasXmppClient()
     535    },
     536    "optionWonderDuration": {
     537        "hidden": () => g_GameAttributes.settings.GameType != "wonder"
     538    },
     539    "cheatWarningText": {
     540        "hidden": () => !g_IsNetworked || !g_GameAttributes.settings.CheatsEnabled
     541    },
     542    "mapSize": {
     543        "hidden": () => g_GameAttributes.mapType != "random" || !g_IsController
     544    },
     545    "mapSizeText": {
     546        "hidden": () => g_GameAttributes.mapType != "random" || g_IsController
     547    },
     548    "mapSizeDesc": {
     549        "hidden": () => g_GameAttributes.mapType != "random"
     550    },
     551    "cancelGame": {
     552        "tooltip": () => Engine.HasXmppClient() ?
     553            translate("Return to the lobby.") :
     554            translate("Return to the main menu.")
     555    },
     556    "startGame": {
     557        "enabled": () => !g_IsController ||
     558                         Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status ||
     559                                                                        g_PlayerAssignments[guid].player == -1)
     560    },
     561    "civResetButton": {
     562        "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController
     563    }
     564};
     565
     566var g_MiscControlArrays = {
     567    "playerBox": {
     568        "size": (idx) => ({
     569            "left": 0,
     570            "right": "100%",
     571            "top": 32 * idx,
     572            "bottom": 32 * (idx + 1)
     573        })
     574    },
     575    "playerName": {
     576        "caption": (idx) => {
     577            // TODO if (g_PlayerAssignments[message.guid].status) green
     578            return translate(g_DefaultPlayerData[idx].Name);
     579        }
     580    },
     581    "playerColor": {
     582        "sprite": (idx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[idx].Color) + " 100"
     583    },
     584    "playerConfig": {
     585        "hidden": (idx) => g_GameAttributes.settings.PlayerData[idx].AI != "",
     586        "onPress": (idx) => {
     587            openAIConfig(idx);
     588        }
     589    }
     590};
    222591
    223592/**
    224593 * Initializes some globals without touching the GUI.
    225594 *
    226595 * @param {Object} attribs - context data sent by the lobby / mainmenu
    function init(attribs)  
    255624    g_DefaultPlayerData = g_Settings.PlayerDefaults;
    256625    g_DefaultPlayerData.shift();
    257626    for (let i in g_DefaultPlayerData)
    258627        g_DefaultPlayerData[i].Civ = "random";
    259628
     629    supplementDefaults();
     630
    260631    setTimeout(displayGamestateNotifications, 1000);
    261632}
    262633
    263634/**
     635 * Sets default values for all g_GameAttribute settings which don't have a value set.
     636 */
     637function supplementDefaults()
     638{
     639    for (let dropdown in g_Dropdowns)
     640        if (!g_Dropdowns[dropdown].defined())
     641            g_Dropdowns[dropdown].select(g_Dropdowns[dropdown]["default"]());
     642
     643    for (let checkbox in g_Checkboxes)
     644        if (!g_Checkboxes[checkbox].defined())
     645            g_Checkboxes[checkbox].set(g_Checkboxes[checkbox]["default"]());
     646
     647    for (let dropdown in g_DropdownArrays)
     648        for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
     649            if (!g_DropdownArrays[dropdown].defined(i))
     650                g_DropdownArrays[dropdown].select(g_DropdownArrays[dropdown]["default"](i));
     651}
     652
     653/**
    264654 * Called after the first tick.
    265655 */
    266656function initGUIObjects()
    267657{
    268     Engine.GetGUIObjectByName("cancelGame").tooltip = Engine.HasXmppClient() ? translate("Return to the lobby.") : translate("Return to the main menu.");
    269 
    270     initCivNameList();
    271     initMapTypes();
    272     initMapFilters();
    273 
    274     if (g_IsController)
    275     {
    276         g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked;
    277         g_GameAttributes.settings.RatingEnabled = Engine.IsRankedGame() || undefined;
     658    for (let dropdown in g_Dropdowns)
     659        initDropdown(dropdown);
    278660
    279         initMapNameList();
    280         initNumberOfPlayers();
    281         initGameSpeed();
    282         initPopulationCaps();
    283         initStartingResources();
    284         initCeasefire();
    285         initWonderDurations();
    286         initVictoryConditions();
    287         initMapSizes();
    288         initRadioButtons();
    289     }
    290     else
    291         hideControls();
     661    for (let checkbox in g_Checkboxes)
     662        initCheckbox(checkbox);
    292663
    293     initMultiplayerSettings();
    294     initPlayerAssignments();
     664    for (let dropdown in g_DropdownArrays)
     665        initDropdownArray(dropdown);
    295666
    296667    resizeMoreOptionsWindow();
    297668
    298669    if (g_IsNetworked)
    299670        Engine.GetGUIObjectByName("chatInput").focus();
    function initGUIObjects()  
    305676            warn("initGUIObjects() called while in GUI update");
    306677        updateGameAttributes();
    307678    }
    308679}
    309680
    310 function initMapTypes()
     681function initDropdown(name, idx)
    311682{
    312     let mapTypes = Engine.GetGUIObjectByName("mapType");
    313     mapTypes.list = g_MapTypes.Title;
    314     mapTypes.list_data = g_MapTypes.Name;
    315     mapTypes.onSelectionChange = function() {
    316         if (this.selected != -1)
    317             selectMapType(this.list_data[this.selected]);
    318     };
    319     if (g_IsController)
    320         mapTypes.selected = g_MapTypes.Default;
     683    let idxName = idx === undefined ? "": "[" + idx + "]";
     684    let data = (idx === undefined ? g_Dropdowns : g_DropdownArrays)[name];
     685
     686    let dropdown = Engine.GetGUIObjectByName(name + idxName);
     687    dropdown.list = data.labels(idx);
     688    dropdown.list_data = data.ids(idx);
     689
     690    //function(data) {
     691        dropdown.onSelectionChange = function() {
     692
     693            if (!g_IsController ||
     694                g_IsInGuiUpdate ||
     695                !this.list_data[this.selected] ||
     696                data.hidden && data.hidden(idx) ||
     697                data.enabled && !data.enabled(idx) ||
     698                data.maps && data.maps.indexOf(g_GameAttributes.mapType) == -1)
     699                return;
     700
     701            data.select(this.selected);
     702            updateGameAttributes();
     703        };
     704    //}(data, idx);
    321705}
    322706
    323 function initMapFilters()
     707function initCheckbox(name)
    324708{
    325     let mapFilters = Engine.GetGUIObjectByName("mapFilter");
    326     mapFilters.list = g_MapFilters.map(mapFilter => mapFilter.name);
    327     mapFilters.list_data = g_MapFilters.map(mapFilter => mapFilter.id);
    328     mapFilters.onSelectionChange = function() {
    329         if (this.selected != -1)
    330             selectMapFilter(this.list_data[this.selected]);
     709    Engine.GetGUIObjectByName(name).onPress = function() {
     710
     711        let obj = g_Checkboxes[this.name];
     712
     713        if (!g_IsController ||
     714            g_IsInGuiUpdate ||
     715            obj.enabled && !obj.enabled() ||
     716            obj.hidden && obj.hidden())
     717            return;
     718
     719        obj.set(this.checked);
     720        updateGameAttributes();
    331721    };
    332     if (g_IsController)
    333         mapFilters.selected = 0;
    334     g_GameAttributes.mapFilter = "default";
     722}
     723
     724function initDropdownArray(name)
     725{
     726    for (let i = 0; i < g_MaxPlayers; ++i)
     727        initDropdown(name, i);
    335728}
    336729
    337730/**
    338731 * Remove empty space in case of hidden options (like cheats, rating or wonder duration)
    339732 */
    340733function resizeMoreOptionsWindow()
    341734{
    342735    const elementHeight = 30;
    343736
     737    let moreOptions = Engine.GetGUIObjectByName("moreOptions");
    344738    let yPos = undefined;
    345739
    346     for (let guiOption of Engine.GetGUIObjectByName("moreOptions").children)
     740    for (let guiOption of moreOptions.children)
    347741    {
    348742        if (guiOption.name == "moreOptionsLabel")
    349743            continue;
    350744
    351745        let gSize = guiOption.size;
    function resizeMoreOptionsWindow()  
    360754
    361755        yPos += elementHeight;
    362756    }
    363757
    364758    // Resize the vertically centered window containing the options
    365     let moreOptions = Engine.GetGUIObjectByName("moreOptions");
    366759    let mSize = moreOptions.size;
    367760    mSize.bottom = mSize.top + yPos + 20;
    368761    moreOptions.size = mSize;
    369762}
    370763
    371 function initNumberOfPlayers()
    372 {
    373     let playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ..., MaxPlayers
    374     let numPlayers = Engine.GetGUIObjectByName("numPlayers");
    375     numPlayers.list = playersArray;
    376     numPlayers.list_data = playersArray;
    377     numPlayers.onSelectionChange = function() {
    378         if (this.selected != -1)
    379             selectNumPlayers(this.list_data[this.selected]);
    380     };
    381     numPlayers.selected = g_MaxPlayers - 1;
    382 }
    383 
    384 function initGameSpeed()
    385 {
    386     let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
    387     gameSpeed.hidden = false;
    388     Engine.GetGUIObjectByName("gameSpeedText").hidden = true;
    389     gameSpeed.list = g_GameSpeeds.Title;
    390     gameSpeed.list_data = g_GameSpeeds.Speed;
    391     gameSpeed.onSelectionChange = function() {
    392         if (this.selected != -1)
    393             g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[this.selected];
    394 
    395         updateGameAttributes();
    396     };
    397     gameSpeed.selected = g_GameSpeeds.Default;
    398 }
    399 
    400 function initPopulationCaps()
    401 {
    402     let populationCaps = Engine.GetGUIObjectByName("populationCap");
    403     populationCaps.list = g_PopulationCapacities.Title;
    404     populationCaps.list_data = g_PopulationCapacities.Population;
    405     populationCaps.selected = g_PopulationCapacities.Default;
    406     populationCaps.onSelectionChange = function() {
    407         if (this.selected != -1)
    408             g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[this.selected];
    409 
    410         updateGameAttributes();
    411     };
    412 }
    413 
    414 function initStartingResources()
    415 {
    416     let startingResourcesL = Engine.GetGUIObjectByName("startingResources");
    417     startingResourcesL.list = g_StartingResources.Title;
    418     startingResourcesL.list_data = g_StartingResources.Resources;
    419     startingResourcesL.selected = g_StartingResources.Default;
    420     startingResourcesL.onSelectionChange = function() {
    421         if (this.selected != -1)
    422             g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[this.selected];
    423 
    424         updateGameAttributes();
    425     };
    426 }
    427 
    428 function initCeasefire()
    429 {
    430     let ceasefireL = Engine.GetGUIObjectByName("ceasefire");
    431     ceasefireL.list = g_Ceasefire.Title;
    432     ceasefireL.list_data = g_Ceasefire.Duration;
    433     ceasefireL.selected = g_Ceasefire.Default;
    434     ceasefireL.onSelectionChange = function() {
    435         if (this.selected != -1)
    436             g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[this.selected];
    437 
    438         updateGameAttributes();
    439     };
    440 }
    441 
    442 function initVictoryConditions()
    443 {
    444     let victoryConditions = Engine.GetGUIObjectByName("victoryCondition");
    445     victoryConditions.list = g_VictoryConditions.Title;
    446     victoryConditions.list_data = g_VictoryConditions.Name;
    447     victoryConditions.onSelectionChange = function() {
    448         if (this.selected != -1)
    449         {
    450             g_GameAttributes.settings.GameType = g_VictoryConditions.Name[this.selected];
    451             g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[this.selected];
    452         }
    453 
    454         updateGameAttributes();
    455     };
    456     victoryConditions.selected = g_VictoryConditions.Default;
    457 }
    458 
    459 function initWonderDurations()
    460 {
    461     let wonderConditions = Engine.GetGUIObjectByName("wonderDuration");
    462     wonderConditions.list = g_WonderDurations.Title;
    463     wonderConditions.list_data = g_WonderDurations.Duration;
    464     wonderConditions.onSelectionChange = function()
    465     {
    466         if (this.selected != -1)
    467             g_GameAttributes.settings.WonderDuration = g_WonderDurations.Duration[this.selected];
    468 
    469         updateGameAttributes();
    470     };
    471     wonderConditions.selected = g_WonderDurations.Default;
    472 }
    473 
    474 function initMapSizes()
    475 {
    476     let mapSize = Engine.GetGUIObjectByName("mapSize");
    477     mapSize.list = g_MapSizes.LongName;
    478     mapSize.list_data = g_MapSizes.Tiles;
    479     mapSize.onSelectionChange = function() {
    480         if (this.selected != -1)
    481             g_GameAttributes.settings.Size = g_MapSizes.Tiles[this.selected];
    482         updateGameAttributes();
    483     };
    484     mapSize.selected = 0;
    485 }
    486 
    487 /**
    488  * Assign update-functions to all checkboxes.
    489  */
    490 function initRadioButtons()
    491 {
    492     let options = {
    493         "RevealMap": "revealMap",
    494         "ExploreMap": "exploreMap",
    495         "DisableTreasures": "disableTreasures",
    496         "LockTeams": "lockTeams",
    497         "CheatsEnabled": "enableCheats"
    498     };
    499 
    500     Object.keys(options).forEach(attribute => {
    501         Engine.GetGUIObjectByName(options[attribute]).onPress = function() {
    502             g_GameAttributes.settings[attribute] = this.checked;
    503             updateGameAttributes();
    504         };
    505     });
    506 
    507     Engine.GetGUIObjectByName("enableRating").onPress = function() {
    508         g_GameAttributes.settings.RatingEnabled = this.checked;
    509         Engine.SetRankedGame(this.checked);
    510         Engine.GetGUIObjectByName("enableCheats").enabled = !this.checked;
    511         Engine.GetGUIObjectByName("lockTeams").enabled = !this.checked;
    512         updateGameAttributes();
    513     };
    514 }
    515 
    516 /**
    517  * If we're a network client, hide the controls and show the text instead.
    518  */
    519 function hideControls()
    520 {
    521     for (let ctrl of ["mapType", "mapFilter", "mapSelection", "victoryCondition", "gameSpeed", "numPlayers"])
    522         hideControl(ctrl, ctrl + "Text");
    523 
    524     // TODO: Shouldn't players be able to choose their own assignment?
    525     for (let i = 0; i < g_MaxPlayers; ++i)
    526     {
    527         Engine.GetGUIObjectByName("playerAssignment["+i+"]").hidden = true;
    528         Engine.GetGUIObjectByName("playerCiv["+i+"]").hidden = true;
    529         Engine.GetGUIObjectByName("playerTeam["+i+"]").hidden = true;
    530     }
    531 
    532     Engine.GetGUIObjectByName("startGame").enabled = true;
    533 }
    534 
    535 /**
    536  * Hides the GUI controls for clients and shows the read-only label instead.
    537  *
    538  * @param {string} control - name of the GUI object able to change a setting
    539  * @param {string} label - name of the GUI object displaying a setting
    540  * @param {boolean} [allowControl] - Whether the current user is allowed to change the control.
    541  */
    542 function hideControl(control, label, allowControl = g_IsController)
    543 {
    544     Engine.GetGUIObjectByName(control).hidden = !allowControl;
    545     Engine.GetGUIObjectByName(label).hidden = allowControl;
    546 }
    547 
    548 /**
    549  * Checks a boolean checkbox for the host and sets the text of the label for the client.
    550  *
    551  * @param {string} control - name of the GUI object able to change a setting
    552  * @param {string} label - name of the GUI object displaying a setting
    553  * @param {boolean} checked - Whether the setting is active / enabled.
    554  */
    555 function setGUIBoolean(control, label, checked)
    556 {
    557     Engine.GetGUIObjectByName(control).checked = checked;
    558     Engine.GetGUIObjectByName(label).caption = checked ? translate("Yes") : translate("No");
    559 }
    560 
    561 /**
    562  * Hide and set some elements depending on whether we play single- or multiplayer.
    563  */
    564 function initMultiplayerSettings()
    565 {
    566     Engine.GetGUIObjectByName("chatPanel").hidden = !g_IsNetworked;
    567     Engine.GetGUIObjectByName("optionCheats").hidden = !g_IsNetworked;
    568     Engine.GetGUIObjectByName("optionRating").hidden = !Engine.HasXmppClient();
    569 
    570     Engine.GetGUIObjectByName("enableCheats").enabled = !Engine.IsRankedGame();
    571     Engine.GetGUIObjectByName("lockTeams").enabled = !Engine.IsRankedGame();
    572 
    573     Engine.GetGUIObjectByName("enableCheats").checked = g_GameAttributes.settings.CheatsEnabled;
    574     Engine.GetGUIObjectByName("enableRating").checked = !!g_GameAttributes.settings.RatingEnabled;
    575 
    576     for (let ctrl of ["enableCheats", "enableRating"])
    577         hideControl(ctrl, ctrl + "Text");
    578 }
    579 
    580 /**
    581  * Populate team-, color- and civ-dropdowns.
    582  */
    583 function initPlayerAssignments()
    584 {
    585     let boxSpacing = 32;
    586     for (let i = 0; i < g_MaxPlayers; ++i)
    587     {
    588         let box = Engine.GetGUIObjectByName("playerBox["+i+"]");
    589         let boxSize = box.size;
    590         let h = boxSize.bottom - boxSize.top;
    591         boxSize.top = i * boxSpacing;
    592         boxSize.bottom = i * boxSpacing + h;
    593         box.size = boxSize;
    594 
    595         let team = Engine.GetGUIObjectByName("playerTeam["+i+"]");
    596         let teamsArray = Array(g_MaxTeams).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxTeams
    597         team.list = [translateWithContext("team", "None")].concat(teamsArray); // "None", 1, 2, ..., maxTeams
    598         team.list_data = [-1].concat(teamsArray.map(team => team - 1)); // -1, 0, ..., (maxTeams-1)
    599         team.selected = 0;
    600 
    601         let playerSlot = i; // declare for inner function use
    602         team.onSelectionChange = function() {
    603             if (this.selected != -1)
    604                 g_GameAttributes.settings.PlayerData[playerSlot].Team = this.selected - 1;
    605 
    606             updateGameAttributes();
    607         };
    608 
    609         let colorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
    610         colorPicker.list = g_PlayerColors.map(color => ' ' + '[color="' + rgbToGuiColor(color) + '"]■[/color]');
    611         colorPicker.list_data = g_PlayerColors.map((color, index) => index);
    612         colorPicker.selected = -1;
    613         colorPicker.onSelectionChange = function() { selectPlayerColor(playerSlot, this.selected); };
    614 
    615         Engine.GetGUIObjectByName("playerCiv["+i+"]").onSelectionChange = function() {
    616             if ((this.selected != -1)&&(g_GameAttributes.mapType !== "scenario"))
    617                 g_GameAttributes.settings.PlayerData[playerSlot].Civ = this.list_data[this.selected];
    618 
    619             updateGameAttributes();
    620         };
    621     }
    622 }
    623 
    624764/**
    625765 * Called when the client disconnects.
    626766 * The other cases from NetClient should never occur in the gamesetup.
    627767 * @param {Object} message
    628768 */
    function getSetting(settings, defaults,  
    790930
    791931    return undefined;
    792932}
    793933
    794934/**
    795  * Initialize the dropdowns containing all selectable civs (including random).
    796  */
    797 function initCivNameList()
    798 {
    799     let civList = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => ({ "name": g_CivData[civ].Name, "code": civ })).sort(sortNameIgnoreCase);
    800     let civListNames = [g_RandomCiv].concat(civList.map(civ => civ.name));
    801     let civListCodes = ["random"].concat(civList.map(civ => civ.code));
    802 
    803     for (let i = 0; i < g_MaxPlayers; ++i)
    804     {
    805         let civ = Engine.GetGUIObjectByName("playerCiv["+i+"]");
    806         civ.list = civListNames;
    807         civ.list_data = civListCodes;
    808         civ.selected = 0;
    809     }
    810 }
    811 
    812 /**
    813935 * Initialize the dropdown containing all maps for the selected maptype and mapfilter.
    814936 */
    815 function initMapNameList()
     937function reloadMapList()
    816938{
    817939    if (!g_MapPath[g_GameAttributes.mapType])
    818940    {
    819941        error("Unexpected map type: " + g_GameAttributes.mapType);
    820942        return;
    function initMapNameList()  
    822944
    823945    let mapFiles = g_GameAttributes.mapType == "random" ?
    824946        getJSONFileList(g_GameAttributes.mapPath) :
    825947        getXMLFileList(g_GameAttributes.mapPath);
    826948
    827     // Apply map filter, if any defined
     949    if (g_GameAttributes.mapType == "random")
     950        mapList.push({
     951            "file": "random",
     952            "name": '[color="' + g_ColorRandom + '"]' + translateWithContext("map type", "Random") + "[/color]"
     953        });
     954
    828955    // TODO: Should verify these are valid maps before adding to list
     956
    829957    let mapList = [];
    830958    for (let mapFile of mapFiles)
    831959    {
    832960        let file = g_GameAttributes.mapPath + mapFile;
    833961        let mapData = loadMapData(file);
    834         let mapFilter = g_MapFilters.find(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "all"));
     962        let filterID = g_MapFilters.id.find(filter => filter == g_GameAttributes.mapFilter);
     963        let mapFilter = g_MapFilters.filter[filterID] || undefined;
    835964
    836         if (!!mapData.settings && mapFilter && mapFilter.filter(mapData.settings.Keywords || []))
    837             mapList.push({ "name": getMapDisplayName(file), "file": file });
    838     }
    839 
    840     translateObjectKeys(mapList, ["name"]);
    841     mapList.sort(sortNameIgnoreCase);
    842 
    843     let mapListNames = mapList.map(map => map.name);
    844     let mapListFiles = mapList.map(map => map.file);
     965        if (!mapData.settings || mapFilter && !mapFilter(mapData.settings.Keywords || []))
     966            continue;
    845967
    846     // Scenario/skirmish maps have a fixed playercount
    847     if (g_GameAttributes.mapType == "random")
    848     {
    849         mapListNames.unshift(g_RandomMap);
    850         mapListFiles.unshift("random");
     968        mapList.push({
     969            "file": file,
     970            "name": translate(getMapDisplayName(file))
     971        });
    851972    }
    852973
    853     let mapSelectionBox = Engine.GetGUIObjectByName("mapSelection");
    854     mapSelectionBox.list = mapListNames;
    855     mapSelectionBox.list_data = mapListFiles;
    856     mapSelectionBox.onSelectionChange = function() {
    857         if (this.selected != -1)
    858             selectMap(this.list_data[this.selected]);
    859     };
    860     mapSelectionBox.selected = Math.max(0, mapListFiles.indexOf(g_GameAttributes.map || ""));
     974    g_MapList = prepareForDropdown(mapList.sort(sortNameIgnoreCase));
     975    initDropdown("mapSelection")
    861976}
    862977
    863978function loadMapData(name)
    864979{
    865980    if (!name || !g_MapPath[g_GameAttributes.mapType])
    function loadPersistMatchSettings()  
    9221037
    9231038    if (mapSettings.PlayerData)
    9241039        sanitizePlayerData(mapSettings.PlayerData);
    9251040
    9261041    // Reload, as the maptype or mapfilter might have changed
    927     initMapNameList();
     1042    reloadMapList();
    9281043
    9291044    g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
    9301045    Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
    9311046
    9321047    updateGUIObjects();
    function onTick()  
    10281143 * Called when the host choses the number of players on a random map.
    10291144 * @param {Number} num
    10301145 */
    10311146function selectNumPlayers(num)
    10321147{
    1033     if (g_IsInGuiUpdate || !g_IsController || g_GameAttributes.mapType != "random")
     1148    if (g_GameAttributes.mapType != "random")
    10341149        return;
    10351150
    10361151    // Unassign players from nonexistent slots
    10371152    if (g_IsNetworked)
    10381153    {
    function selectNumPlayers(num)  
    10481163        g_GameAttributes.settings.PlayerData = pData.slice(0, num);
    10491164    else
    10501165        for (let i = pData.length; i < num; ++i)
    10511166            g_GameAttributes.settings.PlayerData.push(g_DefaultPlayerData[i]);
    10521167
    1053     updateGameAttributes();
    1054 }
    1055 
    1056 /**
    1057  * Assigns the given color to that player.
    1058  */
    1059 function selectPlayerColor(playerSlot, colorIndex)
    1060 {
    1061     if (colorIndex == -1)
    1062         return;
    1063 
    1064     let playerData = g_GameAttributes.settings.PlayerData;
    1065 
    1066     // If someone else has that color, give that player the old color
    1067     let pData = playerData.find(pData => sameColor(g_PlayerColors[colorIndex], pData.Color));
    1068     if (pData)
    1069         pData.Color = playerData[playerSlot].Color;
    1070 
    1071     // Assign the new color
    1072     playerData[playerSlot].Color = g_PlayerColors[colorIndex];
    1073 
    1074     // Ensure colors are not used twice after increasing the number of players
    1075     ensureUniquePlayerColors(playerData);
    1076 
    1077     if (!g_IsInGuiUpdate)
    1078         updateGameAttributes();
     1168    supplementDefaults();
    10791169}
    10801170
    10811171function ensureUniquePlayerColors(playerData)
    10821172{
    10831173    for (let i = playerData.length - 1; i >= 0; --i)
    10841174        // If someone else has that color, assign an unused color
    10851175        if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color)))
    10861176            playerData[i].Color = g_PlayerColors.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
    10871177}
    10881178
    1089 /**
    1090  * Called when the user selects a map type from the list.
    1091  *
    1092  * @param {string} type - scenario, skirmish or random
    1093  */
    1094 function selectMapType(type)
    1095 {
    1096     if (g_IsInGuiUpdate || !g_IsController)
    1097         return;
    1098 
    1099     if (!g_MapPath[type])
    1100     {
    1101         error("selectMapType: Unexpected map type " + type);
    1102         return;
    1103     }
    1104 
    1105     g_MapData = {};
    1106     g_GameAttributes.map = "";
    1107     g_GameAttributes.mapType = type;
    1108     g_GameAttributes.mapPath = g_MapPath[type];
    1109 
    1110     if (type != "scenario")
    1111         g_GameAttributes.settings = {
    1112             "PlayerData": g_DefaultPlayerData.slice(0, 4),
    1113             "CheatsEnabled": g_GameAttributes.settings.CheatsEnabled
    1114         };
    1115 
    1116     initMapNameList();
    1117 
    1118     updateGameAttributes();
    1119 }
    1120 
    1121 function selectMapFilter(id)
    1122 {
    1123     if (g_IsInGuiUpdate || !g_IsController)
    1124         return;
    1125 
    1126     g_GameAttributes.mapFilter = id;
    1127 
    1128     initMapNameList();
    1129 
    1130     updateGameAttributes();
    1131 }
    1132 
    11331179function selectMap(name)
    11341180{
    1135     if (g_IsInGuiUpdate || !g_IsController || !name)
    1136         return;
    1137 
    11381181    // Reset some map specific properties which are not necessarily redefined on each map
    11391182    for (let prop of ["TriggerScripts", "CircularMap", "Garrison"])
    11401183        g_GameAttributes.settings[prop] = undefined;
    11411184
    11421185    let mapData = loadMapData(name);
    function selectMap(name)  
    11641207            g_GameAttributes.settings[prop] = mapSettings[prop];
    11651208
    11661209    // Use default AI if the map doesn't specify any explicitly
    11671210    for (let i in g_GameAttributes.settings.PlayerData)
    11681211    {
    1169         if (!('AI' in g_GameAttributes.settings.PlayerData[i]))
     1212        // TODO: check for undefined?
     1213        if (!g_GameAttributes.settings.PlayerData[i].AI)
    11701214            g_GameAttributes.settings.PlayerData[i].AI = g_DefaultPlayerData[i].AI;
    1171         if (!('AIDiff' in g_GameAttributes.settings.PlayerData[i]))
     1215        if (!g_GameAttributes.settings.PlayerData[i].AIDiff)
    11721216            g_GameAttributes.settings.PlayerData[i].AIDiff = g_DefaultPlayerData[i].AIDiff;
    11731217    }
    11741218
    11751219    if (g_IsNetworked)
    11761220        // Unassign excess players
    function selectMap(name)  
    11881232                "civ": "",
    11891233                "team": -1,
    11901234                "ready": 0
    11911235            }
    11921236        };
    1193 
    1194     updateGameAttributes();
    11951237}
    11961238
    11971239function launchGame()
    11981240{
    11991241    if (!g_IsController)
    function launchGame()  
    12981340        });
    12991341    }
    13001342}
    13011343
    13021344/**
     1345 * Check can be moved to hidden() once it's not identical for all control-arrays anymore
     1346 */
     1347function hideControlArrayElement(idx)
     1348{
     1349    return idx !== undefined && idx >= g_GameAttributes.settings.PlayerData.length;
     1350}
     1351
     1352/**
     1353 * @param idx - Only specified for dropdown arrays.
     1354 */
     1355function updateGUIDropdown(name, idx = undefined)
     1356{
     1357    let idxName = idx === undefined ? "": "[" + idx + "]";
     1358    let obj = (idx === undefined ? g_Dropdowns : g_DropdownArrays)[name];
     1359
     1360    // Naming convention
     1361    let dropdown = Engine.GetGUIObjectByName(name + idxName);
     1362    let label = Engine.GetGUIObjectByName(name + "Text" + idxName);
     1363
     1364    let indexHidden = hideControlArrayElement(idx);
     1365
     1366    // TODO use hideControlArrayElement to skip evil indices
     1367
     1368    let selected = indexHidden ? -1 : dropdown.list_data.indexOf(String(obj.get(idx)));
     1369    let enabled = !indexHidden && (!obj.enabled || obj.enabled(idx));
     1370    let hidden = indexHidden || obj.hidden && obj.hidden(idx);
     1371
     1372    dropdown.hidden = !g_IsController || !enabled || hidden;
     1373    dropdown.selected = selected;
     1374
     1375    if (label)
     1376    {
     1377        label.hidden = g_IsController && enabled || hidden;
     1378        label.caption = selected == -1 ? translate("Unknown") : dropdown.list[selected];
     1379    }
     1380}
     1381
     1382function updateGUICheckbox(name)
     1383{
     1384    let obj = g_Checkboxes[name];
     1385
     1386    let checked = obj.get();
     1387    let hidden = obj.hidden && obj.hidden();
     1388    let enabled = !obj.enabled || obj.enabled();
     1389
     1390    // Naming convention
     1391    let checkbox = Engine.GetGUIObjectByName(name);
     1392    let label = Engine.GetGUIObjectByName(name + "Text");
     1393
     1394    checkbox.checked = checked;
     1395    checkbox.enabled = enabled;
     1396    checkbox.hidden = hidden || !g_IsController;
     1397
     1398    label.caption = checked ? translate("Yes") : translate("No");
     1399    label.hidden = hidden || g_IsController;
     1400}
     1401
     1402function updateGUIMiscControl(name, idx)
     1403{
     1404    let idxName = idx === undefined ? "": "[" + idx + "]";
     1405
     1406    let obj = g_MiscControls[name];
     1407    let control = Engine.GetGUIObjectByName(name + idxName);
     1408
     1409    for (let property in obj)
     1410        control[property] = obj[property]();
     1411
     1412    if (hideControlArrayElement(idx))
     1413        control.hidden = true;
     1414}
     1415
     1416/**
    13031417 * Don't set any attributes here, just show the changes in the GUI.
    13041418 *
    13051419 * Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously.
    13061420 */
    13071421function updateGUIObjects()
    13081422{
    13091423    g_IsInGuiUpdate = true;
    13101424
    1311     let mapSettings = g_GameAttributes.settings;
    1312 
    1313     // These dropdowns don't set values while g_IsInGuiUpdate
    1314     let mapName = g_GameAttributes.map || "";
    1315     let mapFilterIdx = g_MapFilters.findIndex(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "default"));
    1316     let mapTypeIdx = g_GameAttributes.mapType !== undefined ? g_MapTypes.Name.indexOf(g_GameAttributes.mapType) : g_MapTypes.Default;
    1317     let gameSpeedIdx = g_GameAttributes.gameSpeed !== undefined ? g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) : g_GameSpeeds.Default;
    1318 
    1319     // These dropdowns might set the default (as they ignore g_IsInGuiUpdate)
    1320     let mapSizeIdx = mapSettings.Size !== undefined ? g_MapSizes.Tiles.indexOf(mapSettings.Size) : g_MapSizes.Default;
    1321     let victoryIdx = mapSettings.GameType !== undefined ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
    1322     let wonderDurationIdx = mapSettings.WonderDuration !== undefined ? g_WonderDurations.Duration.indexOf(mapSettings.WonderDuration) : g_WonderDurations.Default;
    1323     let popIdx = mapSettings.PopulationCap !== undefined ? g_PopulationCapacities.Population.indexOf(mapSettings.PopulationCap) : g_PopulationCapacities.Default;
    1324     let startingResIdx = mapSettings.StartingResources !== undefined ? g_StartingResources.Resources.indexOf(mapSettings.StartingResources) : g_StartingResources.Default;
    1325     let ceasefireIdx = mapSettings.Ceasefire !== undefined ? g_Ceasefire.Duration.indexOf(mapSettings.Ceasefire) : g_Ceasefire.Default;
    1326     let numPlayers = mapSettings.PlayerData ? mapSettings.PlayerData.length : g_MaxPlayers;
     1425    for (let name in g_Dropdowns)
     1426        updateGUIDropdown(name);
    13271427
    1328     if (g_IsController)
    1329     {
    1330         Engine.GetGUIObjectByName("mapType").selected = mapTypeIdx;
    1331         Engine.GetGUIObjectByName("mapFilter").selected = mapFilterIdx;
    1332         Engine.GetGUIObjectByName("mapSelection").selected = Engine.GetGUIObjectByName("mapSelection").list_data.indexOf(mapName);
    1333         Engine.GetGUIObjectByName("mapSize").selected = mapSizeIdx;
    1334         Engine.GetGUIObjectByName("numPlayers").selected = numPlayers - 1;
    1335         Engine.GetGUIObjectByName("victoryCondition").selected = victoryIdx;
    1336         Engine.GetGUIObjectByName("wonderDuration").selected = wonderDurationIdx;
    1337         Engine.GetGUIObjectByName("populationCap").selected = popIdx;
    1338         Engine.GetGUIObjectByName("gameSpeed").selected = gameSpeedIdx;
    1339         Engine.GetGUIObjectByName("ceasefire").selected = ceasefireIdx;
    1340         Engine.GetGUIObjectByName("startingResources").selected = startingResIdx;
    1341     }
    1342     else
    1343     {
    1344         Engine.GetGUIObjectByName("mapTypeText").caption = g_MapTypes.Title[mapTypeIdx];
    1345         Engine.GetGUIObjectByName("mapFilterText").caption = g_MapFilters[mapFilterIdx].name;
    1346         Engine.GetGUIObjectByName("mapSelectionText").caption = mapName == "random" ? g_RandomMap : translate(getMapDisplayName(mapName));
    1347         initMapNameList();
    1348     }
    1349 
    1350     // Can be visible to both host and clients
    1351     Engine.GetGUIObjectByName("mapSizeText").caption = g_GameAttributes.mapType == "random" ? g_MapSizes.LongName[mapSizeIdx] : translate("Default");
    1352     Engine.GetGUIObjectByName("numPlayersText").caption = numPlayers;
    1353     Engine.GetGUIObjectByName("victoryConditionText").caption = g_VictoryConditions.Title[victoryIdx];
    1354     Engine.GetGUIObjectByName("wonderDurationText").caption = g_WonderDurations.Title[wonderDurationIdx];
    1355     Engine.GetGUIObjectByName("populationCapText").caption = g_PopulationCapacities.Title[popIdx];
    1356     Engine.GetGUIObjectByName("startingResourcesText").caption = g_StartingResources.Title[startingResIdx];
    1357     Engine.GetGUIObjectByName("ceasefireText").caption = g_Ceasefire.Title[ceasefireIdx];
    1358     Engine.GetGUIObjectByName("gameSpeedText").caption = g_GameSpeeds.Title[gameSpeedIdx];
    1359 
    1360     setGUIBoolean("enableCheats", "enableCheatsText", !!mapSettings.CheatsEnabled);
    1361     setGUIBoolean("disableTreasures", "disableTreasuresText", !!mapSettings.DisableTreasures);
    1362     setGUIBoolean("exploreMap", "exploreMapText", !!mapSettings.ExploreMap);
    1363     setGUIBoolean("revealMap", "revealMapText", !!mapSettings.RevealMap);
    1364     setGUIBoolean("lockTeams", "lockTeamsText", !!mapSettings.LockTeams);
    1365     setGUIBoolean("enableRating", "enableRatingText", !!mapSettings.RatingEnabled);
    1366 
    1367     Engine.GetGUIObjectByName("optionWonderDuration").hidden =
    1368         g_GameAttributes.settings.GameType &&
    1369         g_GameAttributes.settings.GameType != "wonder";
    1370 
    1371     Engine.GetGUIObjectByName("cheatWarningText").hidden = !g_IsNetworked || !mapSettings.CheatsEnabled;
    1372 
    1373     Engine.GetGUIObjectByName("enableCheats").enabled = !mapSettings.RatingEnabled;
    1374     Engine.GetGUIObjectByName("lockTeams").enabled = !mapSettings.RatingEnabled;
    1375 
    1376     // Mapsize completely hidden for non-random maps
    1377     let isRandom = g_GameAttributes.mapType == "random";
    1378     Engine.GetGUIObjectByName("mapSizeDesc").hidden = !isRandom;
    1379     Engine.GetGUIObjectByName("mapSize").hidden = !isRandom || !g_IsController;
    1380     Engine.GetGUIObjectByName("mapSizeText").hidden = !isRandom || g_IsController;
    1381     hideControl("numPlayers", "numPlayersText", isRandom && g_IsController);
    1382 
    1383     let notScenario = g_GameAttributes.mapType != "scenario" && g_IsController ;
    1384 
    1385     for (let ctrl of ["victoryCondition", "wonderDuration", "populationCap",
    1386                       "startingResources", "ceasefire", "revealMap",
    1387                       "exploreMap", "disableTreasures", "lockTeams"])
    1388         hideControl(ctrl, ctrl + "Text", notScenario);
     1428    for (let name in g_Checkboxes)
     1429        updateGUICheckbox(name);
    13891430
    1390     Engine.GetGUIObjectByName("civResetButton").hidden = !notScenario;
     1431    for (let name in g_MiscControls)
     1432        updateGUIMiscControl(name);
    13911433
    13921434    for (let i = 0; i < g_MaxPlayers; ++i)
    13931435    {
    1394         Engine.GetGUIObjectByName("playerBox["+i+"]").hidden = (i >= numPlayers);
    1395 
    1396         if (i >= numPlayers)
    1397             continue;
     1436        for (let name in g_DropdownArrays)
     1437            updateGUIDropdown(name, i);
    13981438
    1399         let pName = Engine.GetGUIObjectByName("playerName["+i+"]");
    1400         let pAssignment = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
    1401         let pAssignmentText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
    1402         let pCiv = Engine.GetGUIObjectByName("playerCiv["+i+"]");
    1403         let pCivText = Engine.GetGUIObjectByName("playerCivText["+i+"]");
    1404         let pTeam = Engine.GetGUIObjectByName("playerTeam["+i+"]");
    1405         let pTeamText = Engine.GetGUIObjectByName("playerTeamText["+i+"]");
    1406         let pColor = Engine.GetGUIObjectByName("playerColor["+i+"]");
    1407 
    1408         let pData = mapSettings.PlayerData ? mapSettings.PlayerData[i] : {};
    1409         let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[i] : {};
    1410 
    1411         let color = getSetting(pData, pDefs, "Color");
    1412         pColor.sprite = "color:" + rgbToGuiColor(color) + " 100";
    1413         pName.caption = translate(getSetting(pData, pDefs, "Name"));
    1414 
    1415         let team = getSetting(pData, pDefs, "Team");
    1416         let civ = getSetting(pData, pDefs, "Civ");
    1417 
    1418         pAssignmentText.caption = pAssignment.list[0] ? pAssignment.list[Math.max(0, pAssignment.selected)] : translate("Loading...");
    1419         pCivText.caption = civ == "random" ? g_RandomCiv : (g_CivData[civ] ? g_CivData[civ].Name : "Unknown");
    1420         pTeamText.caption = (team !== undefined && team >= 0) ? team+1 : "-";
    1421 
    1422         pCiv.selected = civ ? pCiv.list_data.indexOf(civ) : 0;
    1423         pTeam.selected = team !== undefined && team >= 0 ? team+1 : 0;
    1424 
    1425         hideControl("playerAssignment["+i+"]", "playerAssignmentText["+i+"]", g_IsController);
    1426         hideControl("playerCiv["+i+"]", "playerCivText["+i+"]", notScenario);
    1427         hideControl("playerTeam["+i+"]", "playerTeamText["+i+"]", notScenario);
    1428 
    1429         // Allow host to chose player colors on non-scenario maps
    1430         let pColorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
    1431         let pColorPickerHeading = Engine.GetGUIObjectByName("playerColorHeading");
    1432         let canChangeColors = g_IsController && g_GameAttributes.mapType != "scenario";
    1433         pColorPicker.hidden = !canChangeColors;
    1434         pColorPickerHeading.hidden = !canChangeColors;
    1435         if (canChangeColors)
    1436             pColorPicker.selected = g_PlayerColors.findIndex(col => sameColor(col, color));
     1439        for (let name in g_MiscControlArrays)
     1440            updateGUIMiscControl(name, i);
    14371441    }
    14381442
    14391443    updateMapDescription();
     1444
    14401445    resizeMoreOptionsWindow();
    14411446
    14421447    g_IsInGuiUpdate = false;
    14431448
    14441449    // Game attributes include AI settings, so update the player list
    function AIConfigCallback(ai)  
    15571562    g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
    15581563
    15591564    updateGameAttributes();
    15601565}
    15611566
     1567/**
     1568 * TODO: delete
     1569 */
    15621570function updatePlayerList()
    15631571{
    15641572    g_IsInGuiUpdate = true;
    15651573
    1566     let hostNameList = [];
    1567     let hostGuidList = [];
     1574    let playerChoices = sortGUIDsByPlayerID().map(guid => ({
     1575        "id": "guid:" + guid,
     1576        "label":
     1577            g_PlayerAssignments[guid].player == -1 ?
     1578            "[color=\""+ g_UnassignedPlayerColor + "\"]" + g_PlayerAssignments[guid].name + "[/color]" :
     1579            g_PlayerAssignments[guid].name
     1580    }));
     1581
     1582    let aiChoices = g_Settings.AIDescriptions
     1583        .filter(ai => !ai.data.hidden || !!g_GameAttributes.settings.PlayerData.every(pData => pData.AI != ai.id))
     1584        .map(ai => ({
     1585            "id": "ai:" + ai.id,
     1586            "label": "[color=\""+ g_AIColor + "\"]" +
     1587                      sprintf(translate("AI: %(ai)s"), {
     1588                          "ai": translate(ai.data.name)
     1589                      }) + "[/color]"
     1590    }));
     1591
     1592    let unassignedSlot = [{
     1593        "id": "",
     1594        "label": "[color=\""+ g_UnassignedColor + "\"]" + translate("Unassigned") + "[/color]",
     1595    }];
     1596    m_PlayerAssignmentChoices = playerChoices.concat(aiChoices).concat(unassignedSlot);
     1597
    15681598    let assignments = [];
    15691599    let aiAssignments = {};
    1570     let noAssignment;
    1571     let assignedCount = 0;
    15721600    for (let guid of sortGUIDsByPlayerID())
    1573     {
    1574         let player = g_PlayerAssignments[guid].player;
    1575 
    1576         if (player != -1)
    1577             hostNameList.push(g_PlayerAssignments[guid].name);
    1578         else
    1579             hostNameList.push("[color=\""+ g_UnassignedPlayerColor + "\"]" + g_PlayerAssignments[guid].name + "[/color]");
    1580 
    1581         hostGuidList.push(guid);
    1582         assignments[player] = hostNameList.length-1;
    1583 
    1584         if (player != -1)
    1585             ++assignedCount;
    1586     }
    1587 
    1588     // Only enable start button if we have enough assigned players
    1589     if (g_IsController)
    1590         Engine.GetGUIObjectByName("startGame").enabled = assignedCount > 0;
    1591 
    1592     for (let ai of g_Settings.AIDescriptions)
    1593     {
    1594         // If the map uses a hidden AI then don't hide it
    1595         if (ai.data.hidden && g_GameAttributes.settings.PlayerData.every(pData => pData.AI != ai.id))
    1596             continue;
     1601        assignments[g_PlayerAssignments[guid].player] = g_HostNameList.length - 1;
    15971602
    1598         aiAssignments[ai.id] = hostNameList.length;
    1599         hostNameList.push("[color=\""+ g_AIColor + "\"]" + sprintf(translate("AI: %(ai)s"), { "ai": translate(ai.data.name) }));
    1600         hostGuidList.push("ai:" + ai.id);
    1601     }
    1602 
    1603     noAssignment = hostNameList.length;
    1604     hostNameList.push("[color=\""+ g_UnassignedColor + "\"]" + translate("Unassigned"));
    1605     hostGuidList.push("");
    1606 
    1607     for (let i = 0; i < g_MaxPlayers; ++i)
    1608     {
    1609         let playerSlot = i;
    1610         let playerID = i+1; // we don't show Gaia, so first slot is ID 1
    1611 
    1612         let selection = assignments[playerID];
    1613 
    1614         let configButton = Engine.GetGUIObjectByName("playerConfig["+i+"]");
    1615         configButton.hidden = true;
    1616 
    1617         // Look for valid player slots
    1618         if (playerSlot >= g_GameAttributes.settings.PlayerData.length)
    1619             continue;
    1620 
    1621         // If no human is assigned, look for an AI instead
    1622         if (selection === undefined)
    1623         {
    1624             let aiId = g_GameAttributes.settings.PlayerData[playerSlot].AI;
    1625             if (aiId)
    1626             {
    1627                 // Check for a valid AI
    1628                 if (aiId in aiAssignments)
    1629                 {
    1630                     selection = aiAssignments[aiId];
    1631                     configButton.hidden = false;
    1632                     configButton.onpress = function()
    1633                     {
    1634                         openAIConfig(playerSlot);
    1635                     };
    1636                 }
    1637                 else
    1638                 {
    1639                     g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
    1640                     warn("AI \"" + aiId + "\" not present. Defaulting to unassigned.");
    1641                 }
    1642             }
    1643 
    1644             if (!selection)
    1645                 selection = noAssignment;
    1646         }
    1647         // There was a human, so make sure we don't have any AI left
    1648         // over in their slot, if we're in charge of the attributes
    1649         else if (g_IsController && g_GameAttributes.settings.PlayerData[playerSlot].AI)
    1650         {
    1651             g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
    1652             if (g_IsNetworked)
    1653                 Engine.SetNetworkGameAttributes(g_GameAttributes);
    1654         }
     1603    g_GameAttributes.settings.PlayerData[playerSlot].AI = g_GameAttributes.settings.PlayerData[playerSlot].AI || "";
    16551604
    1656         let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
    1657         let assignBoxText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
    1658         assignBox.list = hostNameList;
    1659         assignBox.list_data = hostGuidList;
    1660         if (assignBox.selected != selection)
    1661             assignBox.selected = selection;
    1662         assignBoxText.caption = hostNameList[selection];
    1663 
    1664         if (g_IsController)
    1665             assignBox.onselectionchange = function() {
    1666                 if (g_IsInGuiUpdate)
    1667                     return;
    1668 
    1669                 let guid = hostGuidList[this.selected];
    1670                 if (!guid)
    1671                 {
    1672                     if (g_IsNetworked)
    1673                         // Unassign any host from this player slot
    1674                         Engine.AssignNetworkPlayer(playerID, "");
    1675                     // Remove AI from this player slot
    1676                     g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
    1677                 }
    1678                 else if (guid.substr(0, 3) == "ai:")
    1679                 {
    1680                     if (g_IsNetworked)
    1681                         // Unassign any host from this player slot
    1682                         Engine.AssignNetworkPlayer(playerID, "");
    1683                     // Set the AI for this player slot
    1684                     g_GameAttributes.settings.PlayerData[playerSlot].AI = guid.substr(3);
    1685                 }
    1686                 else
    1687                     swapPlayers(guid, playerSlot);
    1688 
    1689                 if (g_IsNetworked)
    1690                     Engine.SetNetworkGameAttributes(g_GameAttributes);
    1691                 else
    1692                     updatePlayerList();
    1693                 updateReadyUI();
    1694             };
    1695     }
     1605    initDropdownArray("playerAssignment");
    16961606
    16971607    g_IsInGuiUpdate = false;
    16981608}
    16991609
    17001610function swapPlayers(guid, newSlot)
    function updateReadyUI()  
    18751785
    18761786    // The host is not allowed to start until everyone is ready.
    18771787    if (g_IsNetworked && g_IsController)
    18781788    {
    18791789        let startGameButton = Engine.GetGUIObjectByName("startGame");
    1880         startGameButton.enabled = allReady;
     1790
    18811791        // Add a explanation on to the tooltip if disabled.
    18821792        let disabledIndex = startGameButton.tooltip.indexOf('Disabled');
    18831793        if (disabledIndex != -1 && allReady)
    18841794            startGameButton.tooltip = startGameButton.tooltip.substring(0, disabledIndex - 2);
    18851795        else if (disabledIndex == -1 && !allReady)