0ad/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js

270 lines
6.9 KiB
JavaScript
Raw Normal View History

Gamesetup class rewrite, fixes #5322, refs #5387. * Decouples settings logically which in turn allows fixing many problems arising from previous coupling. * Fixes the persist-match-settings feature, refs #2963, refs #3049. * Improves performance of the matchsetup by rebuilding GUI objects only when necessary. Provides groundwork for: * UI to control per-player handicap, such as StartingResources, PopulationCap, StartingTechnologies, DisabledTechnologies, DisabledTemplates, ..., refs #812. * Map specific settings (onMapChange event), refs #4838. * Chat notifications announcing which settings changed, refs D1195, * Multiple controllers setting up the game (since setting types can check for permissions in onUpdateGameAttributes without the need for a new data model or a second gamesetup data network message type), refs #3806, subsequently dedicated server, refs #3556. * MapBrowser (MapCache, MapTypes, onUpdateGameAttributes interface), refs D1703 and D1777, * Multiplayer saved games (decoupling and setting dependent unique logic), refs #1088. Refs https://wildfiregames.com/forum/index.php?/topic/20787-paid-development-2016/ https://wildfiregames.com/forum/index.php?/topic/20789-paid-development-2016/ Enable maps to restrict setting values: * If a map specifies an AI or Civs for a playerslot, the controller can't assign a player/other AI or Civ to that slot, refs #3049, #3013. Fix per player StartingResources, PopulationCap, StartingTechnologies, DisabledTechnologies, DisabledTemplates following 9177683653, refs #812, fixes #4504. Use this for DisabledTechnologies on Polar Sea. Persist user settings for Skirmish maps: * All user chosen settings are persisted when changing the selected map or maptype, except where the selected map overwrites the setting value and except for Scenario maps which still use the default value where the map doesn't specify the setting value. * Tickets relating to that Skirmish mapchange user setting persistance: - Selecting a map doesn't change the selected civilizations, fixes #3120 (together with 7cf83f19fd removing map specified Civs). - Selecting a map type doesn't reset the selected settings, fixes #5372. - Selecting a map doesn't change the selected victory conditions, unless the map specifies those, refs #4661, #3209. (Atlas still writes VictoryConditions to every map.) - Consume the player color palette from Skirmish maps, refs 4996d28110 / #1580. Preserve the selected playercolors when switching the Skirmish/Random map by chosing the most similar colors if the map comes with a different palette. Rated games: * Hide and disable Rated game setting unless there are exactly two players, fixes #3950, supersedes D2117. * Display conspicuous warning if the game is rated, so players are perfectly aware. Autostarted games: * Allow using the gamesetup page to autostart matches with arbitrary maps, not only this one tutorial, as reported in D194 and 15e2b42525, refs D11. Networking: * Keep gamesetup page open after disconnect, allowing players to read chat messages indicating why the host stopped the server, fixes #4114. * The message subscription system allows new and mod settings to run custom logic on arbitrary setting changes (most importantly on map change). This removes hardcoded logic restrictions from the gamesetup option unification rewrite in b4e5858f6d/D322, refs #3994, such as the hardcoding of setting references in selectMap to biomes from f2550705d3/D852 and the difficulty from 9daa7520ef/D1189, RelicDuration, WonderDuration, LastManStanding, RegicideGarrison, TriggerScripts, CircularMap, Garrison, DisabledTemplates. Checkboxes: * Display values of disabled checkboxes with Yes/No labels, fixes D2349, reviewed by nani. Clean g_GameAttributes of invalid values and gamesetup GUI temporaries, refs #3049, #3883: * Delete useless values: - VictoryScripts, because they are redundant with TriggerScripts, introduced in 8915037631. - mapType which was written twice to g_GameAttributes following 9177683653 - Description, Keywords, Preview since that doesn't impact simulation and can be loaded from the MapCache - mapFilter, mapPath, SupportedBiomes, SupportedTriggerDifficulties since they are only used in the gamesetup * Delete conditional values if the condition is not met: - AIDiff, AIBehavior if there is no AI in that slot - Nomad and Size if the maptype is not Random - Biome, TriggerDifficulty if the map doesn't support that - WonderDuration, RegicideGarrison, RelicCount, RelicDuration if the according VictoryConditions are not enabled - LastManStanding if TeamsLocked - Rating if there are more than 2 players MapCache: * Refactor to MapCache class, store maps of all types and use it in the replaymenu, lobby and session as well. SettingTabsPanel: * Remove hardcodings and coupling of the SettingTabsPanel with biomes/difficulties/chat UI from D1027/ac7b5ce861. GamesetupPage.xml: * Restructure the page to use hierarchical object organization (topPanel, centerPanel, centerLeftPanel, bottomPanel, centerCenterPanel, centerRightPanel, bottomLeftPanel, bottomRightPanel), allowing to deduplicate object position margins and size math and ease navigation. New defaults: * Check LockedTeams default in multiplayer (not only rated games). * Persist the rated game setting instead of defaulting to true when restarting a match, which often lead to unintentional rated games when rehosting. * 60 FPS in menus since they are animated Autocomplete sorting fixed (playernames should be completed first). Refactoring encompasses the one proposed in Polakrity and bb D1651. Differential Revision: https://code.wildfiregames.com/D2483 Tested by: nani Discussed with: * nani for blackbox testing, code architecture, performance and MapBrowser in PMs on 2019-12-19, 2019-12-31, 2020-01-06 * Angen for the simulation diff on http://irclogs.wildfiregames.com/2020-01/2020-01-03-QuakeNet-%230ad-dev.log * bb on SettingsTabPanel on http://irclogs.wildfiregames.com/2020-01/2020-01-05-QuakeNet-%230ad-dev.log * Imarok on data model and revised multi-controller plans for #3806 on http://irclogs.wildfiregames.com/2020-01/2020-01-07-QuakeNet-%230ad-dev.log Emojis by: asterix, Imarok, fpre, nani, Krinkle, Stan, Angen, Freagarach This was SVN commit r23374.
2020-01-11 12:14:17 -08:00
/**
* This class provides a property independent interface to g_GameAttributes events.
* Classes may use this interface in order to react to changing g_GameAttributes.
*/
class GameSettingsControl
{
constructor(gamesetupPage, netMessages, startGameControl, mapCache)
{
this.startGameControl = startGameControl;
this.mapCache = mapCache;
this.gameSettingsFile = new GameSettingsFile(gamesetupPage);
this.previousMap = undefined;
this.depth = 0;
// This property may be read from publicly
this.autostart = false;
this.gameAttributesChangeHandlers = new Set();
this.gameAttributesBatchChangeHandlers = new Set();
this.gameAttributesFinalizeHandlers = new Set();
this.pickRandomItemsHandlers = new Set();
this.assignPlayerHandlers = new Set();
this.mapChangeHandlers = new Set();
gamesetupPage.registerLoadHandler(this.onLoad.bind(this));
gamesetupPage.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this));
startGameControl.registerLaunchGameHandler(this.onLaunchGame.bind(this));
if (g_IsNetworked)
netMessages.registerNetMessageHandler("gamesetup", this.onGamesetupMessage.bind(this));
}
registerMapChangeHandler(handler)
{
this.mapChangeHandlers.add(handler);
}
unregisterMapChangeHandler(handler)
{
this.mapChangeHandlers.delete(handler);
}
/**
* This message is triggered everytime g_GameAttributes change.
* Handlers may subsequently change g_GameAttributes and trigger this message again.
*/
registerGameAttributesChangeHandler(handler)
{
this.gameAttributesChangeHandlers.add(handler);
}
unregisterGameAttributesChangeHandler(handler)
{
this.gameAttributesChangeHandlers.delete(handler);
}
/**
* This message is triggered after g_GameAttributes changed and recursed gameAttributesChangeHandlers finished.
* The use case for this is to update GUI objects which do not change g_GameAttributes but only display the attributes.
*/
registerGameAttributesBatchChangeHandler(handler)
{
this.gameAttributesBatchChangeHandlers.add(handler);
}
unregisterGameAttributesBatchChangeHandler(handler)
{
this.gameAttributesBatchChangeHandlers.delete(handler);
}
registerGameAttributesFinalizeHandler(handler)
{
this.gameAttributesFinalizeHandlers.add(handler);
}
unregisterGameAttributesFinalizeHandler(handler)
{
this.gameAttributesFinalizeHandlers.delete(handler);
}
registerAssignPlayerHandler(handler)
{
this.assignPlayerHandlers.add(handler);
}
unregisterAssignPlayerHandler(handler)
{
this.assignPlayerHandlers.delete(handler);
}
registerPickRandomItemsHandler(handler)
{
this.pickRandomItemsHandlers.add(handler);
}
unregisterPickRandomItemsHandler(handler)
{
this.pickRandomItemsHandlers.delete(handler);
}
onLoad(initData, hotloadData)
{
if (initData && initData.map && initData.mapType)
{
Object.defineProperty(this, "autostart", {
"value": true,
"writable": false,
"configurable": false
});
// TODO: Fix g_GameAttributes, g_GameAttributes.settings,
// g_GameAttributes.settings.PlayerData object references and
// copy over each attribute individually when receiving
// settings from the server or the local file.
g_GameAttributes = {
"mapType": initData.mapType,
"map": initData.map
};
this.updateGameAttributes();
// Don't launchGame before all Load handlers finished
}
else
{
if (hotloadData)
g_GameAttributes = hotloadData.gameAttributes;
else if (g_IsController && this.gameSettingsFile.enabled)
g_GameAttributes = this.gameSettingsFile.loadFile();
this.updateGameAttributes();
this.setNetworkGameAttributes();
}
}
onGetHotloadData(object)
{
object.gameAttributes = g_GameAttributes;
}
onGamesetupMessage(message)
{
if (!message.data)
return;
g_GameAttributes = message.data;
this.updateGameAttributes();
}
/**
* This is to be called whenever g_GameAttributes has been changed except on gameAttributes finalization.
*/
updateGameAttributes()
{
if (this.depth == 0)
Engine.ProfileStart("updateGameAttributes");
if (this.depth >= this.MaxDepth)
{
error("Infinite loop: " + new Error().stack);
Engine.ProfileStop();
return;
}
++this.depth;
// Basic sanitization
{
if (!g_GameAttributes.settings)
g_GameAttributes.settings = {};
if (!g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData = new Array(this.DefaultPlayerCount);
for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
if (!g_GameAttributes.settings.PlayerData[i])
g_GameAttributes.settings.PlayerData[i] = {};
}
// Map change handlers are triggered first, so that GameSettingControls can update their
// gameAttributes model prior to applying that model in their gameAttributesChangeHandler.
if (g_GameAttributes.map && this.previousMap != g_GameAttributes.map && g_GameAttributes.mapType)
{
this.previousMap = g_GameAttributes.map;
let mapData = this.mapCache.getMapData(g_GameAttributes.mapType, g_GameAttributes.map);
for (let handler of this.mapChangeHandlers)
handler(mapData);
}
for (let handler of this.gameAttributesChangeHandlers)
handler();
--this.depth;
if (this.depth == 0)
{
for (let handler of this.gameAttributesBatchChangeHandlers)
handler();
Engine.ProfileStop();
}
}
/**
* This function is to be called when a GUI control has initiated a value change.
*
* To avoid an infinite loop, do not call this function when a gamesetup message was
* received and the data had only been modified deterministically.
*/
setNetworkGameAttributes()
{
if (g_IsNetworked)
Engine.SetNetworkGameAttributes(g_GameAttributes);
}
getPlayerData(gameAttributes, playerIndex)
{
return gameAttributes &&
gameAttributes.settings &&
gameAttributes.settings.PlayerData &&
gameAttributes.settings.PlayerData[playerIndex] || undefined;
}
assignPlayer(sourcePlayerIndex, playerIndex)
{
if (playerIndex == -1)
return;
let target = this.getPlayerData(g_GameAttributes, playerIndex);
let source = this.getPlayerData(g_GameAttributes, sourcePlayerIndex);
for (let handler of this.assignPlayerHandlers)
handler(source, target);
this.updateGameAttributes();
this.setNetworkGameAttributes();
}
/**
* This function is called everytime a random setting selection was resolved,
* so that subsequent random settings are triggered too,
* for example picking a random biome after picking a random map.
*/
pickRandomItems()
{
for (let handler of this.pickRandomItemsHandlers)
handler();
}
onLaunchGame()
{
if (!this.autostart)
this.gameSettingsFile.saveFile();
this.pickRandomItems();
for (let handler of this.gameAttributesFinalizeHandlers)
handler();
this.setNetworkGameAttributes();
}
}
GameSettingsControl.prototype.MaxDepth = 512;
/**
* This number is used when selecting the random map type, which doesn't provide PlayerData.
*/
GameSettingsControl.prototype.DefaultPlayerCount = 4;