From 34138a7764b82e5dd932789413c73a2468545aa2 Mon Sep 17 00:00:00 2001 From: elexis Date: Sat, 11 Jan 2020 20:14:17 +0000 Subject: [PATCH] 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. --- binaries/data/config/default.cfg | 2 +- .../mods/mod/gui/common/modern/sprites.xml | 12 +- .../data/mods/public/gui/aiconfig/aiconfig.js | 6 +- .../data/mods/public/gui/common/MapCache.js | 100 + .../public/gui/common/functions_utility.js | 5 - .../mods/public/gui/common/gamedescription.js | 123 +- .../data/mods/public/gui/common/settings.js | 17 +- .../gamesetup/Controls/GameSettingsControl.js | 269 ++ .../gamesetup/Controls/GameSettingsFile.js | 64 + .../gui/gamesetup/Controls/MapFilters.js | 119 + .../Controls/PlayerAssignmentsControl.js | 164 + .../gui/gamesetup/Controls/ReadyControl.js | 126 + .../gamesetup/Controls/StartGameControl.js | 53 + .../GameSettings/GameSettingControl.js | 178 ++ .../GameSettingControlCheckbox.js | 60 + .../GameSettingControlCheckbox.xml | 14 + .../GameSettingControlDropdown.js | 64 + .../GameSettingControlDropdown.xml | 14 + .../GameSettings/GameSettingControlLabels.xml | 20 + .../GameSettings/GameSettingControlManager.js | 66 + .../GameSettings/GameSettingsLayout.js | 46 + .../GameSettings/PerPlayer/AIConfigButton.js | 188 ++ .../PerPlayer/Dropdowns/PlayerAssignment.js | 269 ++ .../PerPlayer/Dropdowns/PlayerCiv.js | 142 + .../PerPlayer/Dropdowns/PlayerColor.js | 175 ++ .../PerPlayer/Dropdowns/PlayerTeam.js | 78 + .../GameSettings/PerPlayer/PlayerFrame.js | 23 + .../GameSettings/PerPlayer/PlayerName.js | 129 + .../GameSettings/PerPlayer/PlayerSettings.js | 45 + .../GameSettings/PerPlayer/PlayersPanel.xml | 55 + .../PlayerSettingControlManager.js | 29 + .../GameSettings/Single/Checkboxes/Cheats.js | 43 + .../Single/Checkboxes/ExploredMap.js | 58 + .../Single/Checkboxes/LastManStanding.js | 61 + .../Single/Checkboxes/LockedTeams.js | 62 + .../GameSettings/Single/Checkboxes/Nomad.js | 63 + .../GameSettings/Single/Checkboxes/Rating.js | 54 + .../Single/Checkboxes/RegicideGarrison.js | 67 + .../Single/Checkboxes/RevealedMap.js | 53 + .../GameSettings/Single/Checkboxes/Spies.js | 45 + .../Single/Checkboxes/Treasures.js | 45 + .../GameSettings/Single/Dropdowns/Biome.js | 133 + .../Single/Dropdowns/Ceasefire.js | 55 + .../Single/Dropdowns/GameSpeed.js | 76 + .../Single/Dropdowns/MapFilter.js | 94 + .../Single/Dropdowns/MapSelection.js | 171 + .../GameSettings/Single/Dropdowns/MapSize.js | 79 + .../GameSettings/Single/Dropdowns/MapType.js | 66 + .../Single/Dropdowns/PlayerCount.js | 52 + .../Single/Dropdowns/PopulationCap.js | 101 + .../Single/Dropdowns/RelicCount.js | 85 + .../Single/Dropdowns/RelicDuration.js | 85 + .../Single/Dropdowns/StartingResources.js | 92 + .../Single/Dropdowns/TriggerDifficulty.js | 84 + .../Single/Dropdowns/WonderDuration.js | 85 + .../gui/gamesetup/GameSettings/Single/Seed.js | 12 + .../GameSettings/VictoryConditionCheckbox.js | 105 + .../public/gui/gamesetup/GameSetupPage.js | 147 + .../public/gui/gamesetup/GameSetupPage.xml | 82 + .../NetMessages/GameRegisterStanza.js | 146 + .../gui/gamesetup/NetMessages/NetMessages.js | 71 + .../gamesetup/Panels/Buttons/CancelButton.js | 47 + .../gamesetup/Panels/Buttons/CancelButton.xml | 8 + .../gamesetup/Panels/Buttons/CivInfoButton.js | 48 + .../gamesetup/Panels/Buttons/LobbyButton.js | 19 + .../gamesetup/Panels/Buttons/LobbyButton.xml | 11 + .../gamesetup/Panels/Buttons/ReadyButton.js | 88 + .../gamesetup/Panels/Buttons/ReadyButton.xml | 8 + .../Panels/Buttons/ResetCivsButton.js | 36 + .../Panels/Buttons/ResetTeamsButton.js | 35 + .../Panels/Buttons/StartGameButton.js | 72 + .../Panels/Buttons/StartGameButton.xml | 8 + .../Panels/Chat/ChatInputAutocomplete.js | 37 + .../gamesetup/Panels/Chat/ChatInputPanel.js | 55 + .../Panels/Chat/ChatMessages/ClientChat.js | 53 + .../Chat/ChatMessages/ClientConnection.js | 30 + .../Panels/Chat/ChatMessages/ClientKicked.js | 25 + .../Panels/Chat/ChatMessages/ClientReady.js | 30 + .../Chat/ChatMessages/GameSettingsChanged.js | 22 + .../Panels/Chat/ChatMessagesPanel.js | 81 + .../gui/gamesetup/Panels/Chat/ChatPanel.js | 26 + .../gui/gamesetup/Panels/Chat/ChatPanel.xml | 12 + .../Panels/Chat/StatusMessageFormat.js | 28 + .../gamesetup/Panels/Chat/TimestampWrapper.js | 31 + .../gui/gamesetup/Panels/GameDescription.js | 27 + .../gui/gamesetup/Panels/GameDescription.xml | 4 + .../gamesetup/Panels/GameSettingWarning.js | 41 + .../gamesetup/Panels/GameSettingWarning.xml | 2 + .../gui/gamesetup/Panels/GameSettingsPanel.js | 187 ++ .../gamesetup/Panels/GameSettingsPanel.xml | 21 + .../gui/gamesetup/Panels/GameSettingsTabs.js | 97 + .../gui/gamesetup/Panels/GameSettingsTabs.xml | 5 + .../gui/gamesetup/Panels/LoadingWindow.js | 25 + .../gui/gamesetup/Panels/LoadingWindow.xml | 11 + .../public/gui/gamesetup/Panels/MapPreview.js | 24 + .../gui/gamesetup/Panels/MapPreview.xml | 7 + .../gui/gamesetup/Panels/SoundNotification.js | 28 + .../gui/gamesetup/{ai.txt => Panels/Tips.txt} | 0 .../public/gui/gamesetup/Panels/TipsPanel.js | 45 + .../public/gui/gamesetup/Panels/TipsPanel.xml | 14 + .../public/gui/gamesetup/Panels/Tooltip.js | 17 + .../public/gui/gamesetup/Panels/Tooltip.xml | 7 + .../mods/public/gui/gamesetup/gamesetup.js | 2746 +---------------- .../mods/public/gui/gamesetup/gamesetup.xml | 210 +- .../public/gui/loadgame/SavegameDetails.js | 13 +- .../mods/public/gui/lobby/LobbyPage/Game.js | 7 +- .../public/gui/lobby/LobbyPage/GameDetails.js | 16 +- .../public/gui/lobby/LobbyPage/GameList.js | 6 +- .../public/gui/lobby/LobbyPage/LobbyPage.js | 5 +- .../mods/public/gui/pregame/MainMenuItems.js | 5 +- .../mods/public/gui/replaymenu/replay_menu.js | 13 +- .../session/objectives/ObjectivesDialog.js | 4 +- .../data/mods/public/gui/session/session.js | 5 +- .../mods/public/maps/random/jebel_barkal.json | 3 +- .../mods/public/maps/random/polar_sea.json | 5 + .../public/maps/random/polar_sea_triggers.js | 13 - .../data/settings/player_defaults.json | 2 +- .../data/settings/trigger_difficulties.json | 3 +- .../public/simulation/helpers/InitGame.js | 14 +- .../mods/public/simulation/helpers/Player.js | 78 +- 120 files changed, 6434 insertions(+), 3113 deletions(-) create mode 100644 binaries/data/mods/public/gui/common/MapCache.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsFile.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Controls/MapFilters.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Controls/PlayerAssignmentsControl.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Controls/ReadyControl.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Controls/StartGameControl.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControl.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlCheckbox.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlCheckbox.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlDropdown.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlDropdown.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlLabels.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlManager.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingsLayout.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/AIConfigButton.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/Dropdowns/PlayerColor.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/PlayerFrame.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/PlayerName.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/PlayerSettings.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PerPlayer/PlayersPanel.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/PlayerSettingControlManager.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/Cheats.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/ExploredMap.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/LastManStanding.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/LockedTeams.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/Nomad.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/Rating.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/RegicideGarrison.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/RevealedMap.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/Spies.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Checkboxes/Treasures.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/Biome.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/Ceasefire.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/GameSpeed.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/MapFilter.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/MapSelection.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/MapSize.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/MapType.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/PlayerCount.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/PopulationCap.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/RelicCount.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/RelicDuration.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/StartingResources.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/TriggerDifficulty.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Dropdowns/WonderDuration.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/Single/Seed.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSettings/VictoryConditionCheckbox.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSetupPage.js create mode 100644 binaries/data/mods/public/gui/gamesetup/GameSetupPage.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js create mode 100644 binaries/data/mods/public/gui/gamesetup/NetMessages/NetMessages.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/CancelButton.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/CancelButton.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/CivInfoButton.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/LobbyButton.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/LobbyButton.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/ReadyButton.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/ReadyButton.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/ResetCivsButton.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/ResetTeamsButton.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/StartGameButton.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Buttons/StartGameButton.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatInputAutocomplete.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatInputPanel.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatMessages/ClientChat.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatMessages/ClientConnection.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatMessages/ClientKicked.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatMessages/ClientReady.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatMessages/GameSettingsChanged.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatMessagesPanel.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatPanel.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/ChatPanel.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/StatusMessageFormat.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Chat/TimestampWrapper.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/GameDescription.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/GameDescription.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/GameSettingWarning.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/GameSettingWarning.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/GameSettingsPanel.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/GameSettingsPanel.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/GameSettingsTabs.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/GameSettingsTabs.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/LoadingWindow.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/LoadingWindow.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/MapPreview.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/MapPreview.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/SoundNotification.js rename binaries/data/mods/public/gui/gamesetup/{ai.txt => Panels/Tips.txt} (100%) create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/TipsPanel.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/TipsPanel.xml create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Tooltip.js create mode 100644 binaries/data/mods/public/gui/gamesetup/Panels/Tooltip.xml diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index e9f67bb2e2..3351920a22 100644 --- a/binaries/data/config/default.cfg +++ b/binaries/data/config/default.cfg @@ -133,7 +133,7 @@ skycolor = "0 0 0" [adaptivefps] session = 60 ; Throttle FPS in running games (prevents 100% CPU workload). -menu = 30 ; Throttle FPS in menus only. +menu = 60 ; Throttle FPS in menus only. [hotkey] ; Each one of the specified keys will trigger the action on the left diff --git a/binaries/data/mods/mod/gui/common/modern/sprites.xml b/binaries/data/mods/mod/gui/common/modern/sprites.xml index b9941831a9..3d362f392a 100644 --- a/binaries/data/mods/mod/gui/common/modern/sprites.xml +++ b/binaries/data/mods/mod/gui/common/modern/sprites.xml @@ -522,7 +522,7 @@ - Misc. - ========================================== --> - + - - - diff --git a/binaries/data/mods/public/gui/aiconfig/aiconfig.js b/binaries/data/mods/public/gui/aiconfig/aiconfig.js index 5a3bb81d4f..1b025cfd68 100644 --- a/binaries/data/mods/public/gui/aiconfig/aiconfig.js +++ b/binaries/data/mods/public/gui/aiconfig/aiconfig.js @@ -33,16 +33,18 @@ function init(settings) // Remember the player ID that we change the AI settings for g_PlayerSlot = settings.playerSlot; + let enabled = g_IsController && !settings.fixed; + for (let name in g_AIControls) { let control = Engine.GetGUIObjectByName(name); control.list = g_AIControls[name].labels; control.selected = g_AIControls[name].selected(settings); - control.hidden = !g_IsController; + control.hidden = !enabled; let label = Engine.GetGUIObjectByName(name + "Text"); label.caption = control.list[control.selected]; - label.hidden = g_IsController; + label.hidden = enabled; } checkBehavior(); diff --git a/binaries/data/mods/public/gui/common/MapCache.js b/binaries/data/mods/public/gui/common/MapCache.js new file mode 100644 index 0000000000..280e882f39 --- /dev/null +++ b/binaries/data/mods/public/gui/common/MapCache.js @@ -0,0 +1,100 @@ +/** + * This class obtains, caches and provides the gamesettings from map XML and JSON files. + */ +class MapCache +{ + constructor() + { + this.cache = {}; + } + + getMapData(mapType, mapPath) + { + if (!mapPath || mapPath == "random") + return undefined; + + if (!this.cache[mapPath]) + { + let mapData = g_Settings.MapTypes.find(type => type.Name == mapType).GetData(mapPath); + + // Remove gaia, TODO: Maps should be consistent + if (mapData && + mapData.settings && + mapData.settings.PlayerData && + mapData.settings.PlayerData.length && + !mapData.settings.PlayerData[0]) + { + mapData.settings.PlayerData.shift(); + } + + this.cache[mapPath] = mapData; + } + + return this.cache[mapPath]; + } + + /** + * Doesn't translate, so that lobby page viewers can do that locally. + * The result is to be used with translateMapName. + */ + getTranslatableMapName(mapType, mapPath) + { + if (mapPath == "random") + return "random"; + + let mapData = this.getMapData(mapType, mapPath); + return mapData && mapData.settings && mapData.settings.Name || undefined; + } + + translateMapName(mapName) + { + return mapName == "random" ? + translateWithContext("map selection", "Random") : + mapName ? translate(mapName) : ""; + } + + getTranslatedMapDescription(mapType, mapPath) + { + if (mapPath == "random") + return translate("A randomly selected map."); + + let mapData = this.getMapData(mapType, mapPath); + return mapData && mapData.settings && translate(mapData.settings.Description) || ""; + } + + getMapPreview(mapType, mapPath, gameAttributes = undefined) + { + let mapData = this.getMapData(mapType, mapPath); + + let biomePreviewFile = + basename(mapPath) + "_" + + basename(gameAttributes && gameAttributes.settings.Biome || "") + ".png"; + + let biomePreview = Engine.TextureExists( + this.TexturesPath + this.PreviewsPath + biomePreviewFile) && biomePreviewFile; + + let filename = + biomePreview ? + biomePreview : + mapData && mapData.settings && mapData.settings.Preview ? + mapData.settings.Preview : + this.DefaultPreview; + + return "cropped:" + this.PreviewWidth + "," + this.PreviewHeight + ":" + this.PreviewsPath + filename; + } +} + +MapCache.prototype.TexturesPath = + "art/textures/ui/"; + +MapCache.prototype.PreviewsPath = + "session/icons/mappreview/"; + +MapCache.prototype.DefaultPreview = + "nopreview.png"; + +MapCache.prototype.PreviewWidth = + 400 / 512; + +MapCache.prototype.PreviewHeight = + 300 / 512; diff --git a/binaries/data/mods/public/gui/common/functions_utility.js b/binaries/data/mods/public/gui/common/functions_utility.js index 56f5996fa5..9ec6ecbbba 100644 --- a/binaries/data/mods/public/gui/common/functions_utility.js +++ b/binaries/data/mods/public/gui/common/functions_utility.js @@ -114,11 +114,6 @@ function stringifiedTeamListToPlayerData(stringifiedTeamList) return playerData; } -function translateMapTitle(mapTitle) -{ - return mapTitle == "random" ? translateWithContext("map selection", "Random") : translate(mapTitle); -} - function removeDupes(array) { // loop backwards to make splice operations cheaper diff --git a/binaries/data/mods/public/gui/common/gamedescription.js b/binaries/data/mods/public/gui/common/gamedescription.js index 3cdb761dd6..eb8d873933 100644 --- a/binaries/data/mods/public/gui/common/gamedescription.js +++ b/binaries/data/mods/public/gui/common/gamedescription.js @@ -34,55 +34,6 @@ var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyL */ var g_BuddySymbol = '•'; -var g_MapPreviewPath = "session/icons/mappreview/"; - -/** - * Returns the biome specific mappreview image if it exists, or empty string otherwise. - */ -function getBiomePreview(mapName, biomeName) -{ - let biomePreview = basename(mapName) + "_" + basename(biomeName) + ".png"; - - if (Engine.TextureExists("art/textures/ui/" + g_MapPreviewPath + biomePreview)) - return biomePreview; - - return ""; -} - -/** - * Returns map description and preview image or placeholder. - */ -function getMapDescriptionAndPreview(mapType, mapName, gameAttributes = undefined) -{ - let mapData; - if (mapType == "random" && mapName == "random") - mapData = { "settings": { "Description": translate("A randomly selected map.") } }; - else if (mapType == "random" && Engine.FileExists(mapName + ".json")) - mapData = Engine.ReadJSONFile(mapName + ".json"); - else if (Engine.FileExists(mapName + ".xml")) - mapData = Engine.LoadMapSettings(mapName + ".xml"); - - let biomePreview = getBiomePreview(mapName, gameAttributes && gameAttributes.settings.Biome || ""); - - return deepfreeze({ - "description": mapData && mapData.settings && mapData.settings.Description ? translate(mapData.settings.Description) : translate("Sorry, no description available."), - "preview": biomePreview ? biomePreview : - mapData && mapData.settings && mapData.settings.Preview ? mapData.settings.Preview : "nopreview.png" - }); -} - -/** - * Sets the mappreview image correctly. - * It needs to be cropped as the engine only allows loading square textures. - * - * @param {string} filename - */ -function getMapPreviewImage(filename) -{ - return "cropped:" + 400 / 512 + "," + 300 / 512 + ":" + - g_MapPreviewPath + filename; -} - /** * Returns a formatted string describing the player assignments. * Needs g_CivData to translate! @@ -229,7 +180,7 @@ function formatPlayerInfo(playerDataArray, playerStates) * * Requires g_GameAttributes and g_VictoryConditions. */ -function getGameDescription() +function getGameDescription(mapCache) { let titles = []; if (!g_GameAttributes.settings.VictoryConditions.length) @@ -341,13 +292,13 @@ function getGameDescription() { titles.push({ "label": translate("Map Name"), - "value": translate(g_GameAttributes.settings.Name) + "value": mapCache.translateMapName( + mapCache.getTranslatableMapName(g_GameAttributes.mapType, g_GameAttributes.map, g_GameAttributes)) }); + titles.push({ "label": translate("Map Description"), - "value": g_GameAttributes.settings.Description ? - translate(g_GameAttributes.settings.Description) : - translate("Sorry, no description available.") + "value": mapCache.getTranslatedMapDescription(g_GameAttributes.mapType, g_GameAttributes.map) }); } @@ -356,12 +307,6 @@ function getGameDescription() "value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)] }); - if (typeof g_MapFilterList !== "undefined") - titles.push({ - "label": translate("Map Filter"), - "value": g_MapFilterList.name[g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter)] - }); - if (g_GameAttributes.mapType == "random") { let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(g_GameAttributes.settings.Size)]; @@ -390,32 +335,42 @@ function getGameDescription() }); } - titles.push({ - "label": g_GameAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"), - "value": - g_GameAttributes.settings.Nomad ? - translate("Players start with only few units and have to find a suitable place to build their city.") : - translate("Players start with a Civic Center.") - }); + if (g_GameAttributes.settings.Nomad !== undefined) + titles.push({ + "label": g_GameAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"), + "value": + g_GameAttributes.settings.Nomad ? + translate("Players start with only few units and have to find a suitable place to build their city.") : + translate("Players start with a Civic Center.") + }); - titles.push({ - "label": translate("Starting Resources"), - "value": sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), { - "startingResourcesTitle": - g_StartingResources.Title[ - g_StartingResources.Resources.indexOf( - g_GameAttributes.settings.StartingResources)], - "amount": g_GameAttributes.settings.StartingResources - }) - }); + if (g_GameAttributes.settings.StartingResources !== undefined) + titles.push({ + "label": translate("Starting Resources"), + "value": + g_GameAttributes.settings.PlayerData && + g_GameAttributes.settings.PlayerData.some(pData => pData && pData.Resources !== undefined) ? + translateWithContext("starting resources", "Per Player") : + sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), { + "startingResourcesTitle": + g_StartingResources.Title[ + g_StartingResources.Resources.indexOf( + g_GameAttributes.settings.StartingResources)], + "amount": g_GameAttributes.settings.StartingResources + }) + }); - titles.push({ - "label": translate("Population Limit"), - "value": - g_PopulationCapacities.Title[ - g_PopulationCapacities.Population.indexOf( - g_GameAttributes.settings.PopulationCap)] - }); + if (g_GameAttributes.settings.PopulationCap !== undefined) + titles.push({ + "label": translate("Population Limit"), + "value": + g_GameAttributes.settings.PlayerData && + g_GameAttributes.settings.PlayerData.some(pData => pData && pData.PopulationLimit !== undefined) ? + translateWithContext("population limit", "Per Player") : + g_PopulationCapacities.Title[ + g_PopulationCapacities.Population.indexOf( + g_GameAttributes.settings.PopulationCap)] + }); titles.push({ "label": translate("Treasures"), diff --git a/binaries/data/mods/public/gui/common/settings.js b/binaries/data/mods/public/gui/common/settings.js index c660c4d00d..e9e145e875 100644 --- a/binaries/data/mods/public/gui/common/settings.js +++ b/binaries/data/mods/public/gui/common/settings.js @@ -209,8 +209,6 @@ function loadCeasefire() /** * Hardcoded, as modding is not supported without major changes. - * - * @returns {Array} */ function loadMapTypes() { @@ -219,17 +217,26 @@ function loadMapTypes() "Name": "skirmish", "Title": translateWithContext("map", "Skirmish"), "Description": translate("A map with a predefined landscape and number of players. Freely select the other gamesettings."), - "Default": true + "Default": true, + "Path": "maps/skirmishes/", + "Suffix": ".xml", + "GetData": Engine.LoadMapSettings }, { "Name": "random", "Title": translateWithContext("map", "Random"), - "Description": translate("Create a unique map with a different resource distribution each time. Freely select the number of players and teams.") + "Description": translate("Create a unique map with a different resource distribution each time. Freely select the number of players and teams."), + "Path": "maps/random/", + "Suffix": ".json", + "GetData": mapPath => Engine.ReadJSONFile(mapPath + ".json") }, { "Name": "scenario", "Title": translateWithContext("map", "Scenario"), - "Description": translate("A map with a predefined landscape and matchsettings.") + "Description": translate("A map with a predefined landscape and matchsettings."), + "Path": "maps/scenarios/", + "Suffix": ".xml", + "GetData": Engine.LoadMapSettings } ]; } diff --git a/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js b/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js new file mode 100644 index 0000000000..85db9cf645 --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js @@ -0,0 +1,269 @@ +/** + * 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; diff --git a/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsFile.js b/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsFile.js new file mode 100644 index 0000000000..57aa9cba73 --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsFile.js @@ -0,0 +1,64 @@ +/** + * This class provides a way to save g_GameAttributes to a file and load them. + */ +class GameSettingsFile +{ + constructor(gamesetupPage) + { + this.filename = g_IsNetworked ? + this.GameAttributesFileMultiplayer : + this.GameAttributesFileSingleplayer; + + this.engineInfo = Engine.GetEngineInfo(); + this.enabled = Engine.ConfigDB_GetValue("user", this.ConfigName) == "true"; + + gamesetupPage.registerClosePageHandler(this.saveFile.bind(this)); + } + + loadFile() + { + Engine.ProfileStart("loadPersistMatchSettingsFile"); + + let data = + this.enabled && + g_IsController && + Engine.FileExists(this.filename) && + Engine.ReadJSONFile(this.filename); + + let gameAttributes = + data && + data.attributes && + data.engine_info && + data.engine_info.engine_version == this.engineInfo.engine_version && + hasSameMods(data.engine_info.mods, this.engineInfo.mods) && + data.attributes || {}; + + Engine.ProfileStop(); + return gameAttributes; + } + + /** + * Delete settings if disabled, so that players are not confronted with old settings after enabling the setting again. + */ + saveFile() + { + if (!g_IsController) + return; + + Engine.ProfileStart("savePersistMatchSettingsFile"); + Engine.WriteJSONFile(this.filename, { + "attributes": this.enabled ? g_GameAttributes : {}, + "engine_info": this.engineInfo + }); + Engine.ProfileStop(); + } +} + +GameSettingsFile.prototype.ConfigName = + "persistmatchsettings"; + +GameSettingsFile.prototype.GameAttributesFileSingleplayer = + "config/matchsettings.json"; + +GameSettingsFile.prototype.GameAttributesFileMultiplayer = + "config/matchsettings.mp.json"; diff --git a/binaries/data/mods/public/gui/gamesetup/Controls/MapFilters.js b/binaries/data/mods/public/gui/gamesetup/Controls/MapFilters.js new file mode 100644 index 0000000000..713987de7a --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Controls/MapFilters.js @@ -0,0 +1,119 @@ +class MapFilters +{ + constructor(mapCache) + { + this.mapCache = mapCache; + } + + /** + * Some map filters may reject every map of a particular mapType. + * This function allows identifying which map filters have any matches for that maptype. + */ + getAvailableMapFilters(mapTypeName) + { + return this.Filters.filter(filter => + this.getFilteredMaps(mapTypeName, filter.Name, true)); + } + + /** + * This function identifies all maps matching the given mapType and mapFilter. + * If existence is true, it will only test if there is at least one file for that mapType and mapFilter. + * Otherwise it returns an array with filename, translated map title and map description. + */ + getFilteredMaps(mapTypeName, filterName, existence) + { + let index = g_MapTypes.Name.findIndex(name => name == mapTypeName); + if (index == -1) + { + error("Can't get filtered maps for invalid maptype: " + mapTypeName); + return undefined; + } + + let mapFilter = this.Filters.find(filter => filter.Name == filterName); + if (!mapFilter) + { + error("Invalid mapfilter name: " + filterName); + return undefined; + } + + Engine.ProfileStart("getFilteredMaps"); + + let maps = []; + let mapTypePath = g_MapTypes.Path[index]; + for (let filename of listFiles(mapTypePath, g_MapTypes.Suffix[index], false)) + { + if (filename.startsWith(this.HiddenFilesPrefix)) + continue; + + let mapPath = mapTypePath + filename; + let mapData = this.mapCache.getMapData(mapTypeName, mapPath); + + // Map files may come with custom json files + if (!mapData || !mapData.settings) + continue; + + if (MatchesClassList(mapData.settings.Keywords || [], mapFilter.Match)) + { + if (existence) + { + Engine.ProfileStop(); + return true; + } + + maps.push({ + "file": mapPath, + "name": translate(mapData.settings.Name), + "description": translate(mapData.settings.Description) + }); + } + } + + Engine.ProfileStop(); + return existence ? false : maps; + } +} + +/** + * When maps start with this prefix, they will not appear in the maplist. + * Used for the Atlas _default.xml for instance. + */ +MapFilters.prototype.HiddenFilesPrefix = "_"; + +MapFilters.prototype.Filters = [ + { + "Name": "default", + "Title": translate("Default"), + "Description": translate("All maps except naval and demo maps."), + "Match": ["!naval !demo !hidden"] + }, + { + "Name": "naval", + "Title": translate("Naval Maps"), + "Description": translate("Maps where ships are needed to reach the enemy."), + "Match": ["naval"] + }, + { + "Name": "demo", + "Title": translate("Demo Maps"), + "Description": translate("These maps are not playable but for demonstration purposes only."), + "Match": ["demo"] + }, + { + "Name": "new", + "Title": translate("New Maps"), + "Description": translate("Maps that are brand new in this release of the game."), + "Match": ["new"] + }, + { + "Name": "trigger", + "Title": translate("Trigger Maps"), + "Description": translate("Maps that come with scripted events and potentially spawn enemy units."), + "Match": ["trigger"] + }, + { + "Name": "all", + "Title": translate("All Maps"), + "Description": translate("Every map of the chosen maptype."), + "Match": "!" + } +]; diff --git a/binaries/data/mods/public/gui/gamesetup/Controls/PlayerAssignmentsControl.js b/binaries/data/mods/public/gui/gamesetup/Controls/PlayerAssignmentsControl.js new file mode 100644 index 0000000000..6bdced6dff --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Controls/PlayerAssignmentsControl.js @@ -0,0 +1,164 @@ +/** + * This class provides a property independent interface to g_PlayerAssignment events and actions. + */ +class PlayerAssignmentsControl +{ + constructor(gamesetupPage, netMessages) + { + this.clientJoinHandlers = new Set(); + this.clientLeaveHandlers = new Set(); + this.playerAssignmentsChangeHandlers = new Set(); + + if (!g_IsNetworked) + { + let name = singleplayerName(); + + // Replace empty player name when entering a single-player match for the first time. + Engine.ConfigDB_CreateAndWriteValueToFile("user", this.ConfigNameSingleplayer, name, "config/user.cfg"); + + g_PlayerAssignments = { + "local": { + "name": name, + "player": -1 + } + }; + } + + gamesetupPage.registerLoadHandler(this.onLoad.bind(this)); + gamesetupPage.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this)); + netMessages.registerNetMessageHandler("players", this.onPlayerAssignmentMessage.bind(this)); + } + + registerPlayerAssignmentsChangeHandler(handler) + { + this.playerAssignmentsChangeHandlers.add(handler); + } + + unregisterPlayerAssignmentsChangeHandler(handler) + { + this.playerAssignmentsChangeHandlers.delete(handler); + } + + registerClientJoinHandler(handler) + { + this.clientJoinHandlers.add(handler); + } + + unregisterClientJoinHandler(handler) + { + this.clientJoinHandlers.delete(handler); + } + + registerClientLeaveHandler(handler) + { + this.clientLeaveHandlers.add(handler); + } + + unregisterClientLeaveHandler(handler) + { + this.clientLeaveHandlers.delete(handler); + } + + onLoad(initData, hotloadData) + { + if (hotloadData) + { + g_PlayerAssignments = hotloadData.playerAssignments; + this.updatePlayerAssignments(); + } + } + + onGetHotloadData(object) + { + object.playerAssignments = g_PlayerAssignments; + } + + /** + * To be called when g_PlayerAssignments is modified. + */ + updatePlayerAssignments() + { + Engine.ProfileStart("updatePlayerAssignments"); + for (let handler of this.playerAssignmentsChangeHandlers) + handler(); + Engine.ProfileStop(); + } + + /** + * Called whenever a client joins/leaves or any gamesetting is changed. + */ + onPlayerAssignmentMessage(message) + { + let newAssignments = message.newAssignments; + for (let guid in newAssignments) + if (!g_PlayerAssignments[guid]) + for (let handler of this.clientJoinHandlers) + handler(guid, message.newAssignments); + + for (let guid in g_PlayerAssignments) + if (!newAssignments[guid]) + for (let handler of this.clientLeaveHandlers) + handler(guid); + + g_PlayerAssignments = newAssignments; + this.updatePlayerAssignments(); + } + + assignClient(guid, playerIndex) + { + if (g_IsNetworked) + Engine.AssignNetworkPlayer(playerIndex, guid); + else + { + g_PlayerAssignments[guid].player = playerIndex; + this.updatePlayerAssignments(); + } + } + + /** + * If both clients are assigned players, this will swap their assignments. + */ + assignPlayer(guidToAssign, playerIndex) + { + if (g_PlayerAssignments[guidToAssign].player != -1) + for (let guid in g_PlayerAssignments) + if (g_PlayerAssignments[guid].player == playerIndex + 1) + { + this.assignClient(guid, g_PlayerAssignments[guidToAssign].player); + break; + } + + this.assignClient(guidToAssign, playerIndex + 1); + + if (!g_IsNetworked) + this.updatePlayerAssignments(); + } + + unassignClient(playerID) + { + if (g_IsNetworked) + Engine.AssignNetworkPlayer(playerID, ""); + else if (g_PlayerAssignments.local.player == playerID) + { + g_PlayerAssignments.local.player = -1; + this.updatePlayerAssignments(); + } + } + + unassignInvalidPlayers() + { + if (g_IsNetworked) + for (let playerID = g_GameAttributes.settings.PlayerData.length + 1; playerID <= g_MaxPlayers; ++playerID) + // Remove obsolete playerIDs from the servers playerassignments copy + Engine.AssignNetworkPlayer(playerID, ""); + + else if (g_PlayerAssignments.local.player > g_GameAttributes.settings.PlayerData.length) + { + g_PlayerAssignments.local.player = -1; + this.updatePlayerAssignments(); + } + } +} + +PlayerAssignmentsControl.prototype.ConfigNameSingleplayer = + "playername.singleplayer"; diff --git a/binaries/data/mods/public/gui/gamesetup/Controls/ReadyControl.js b/binaries/data/mods/public/gui/gamesetup/Controls/ReadyControl.js new file mode 100644 index 0000000000..a5fab93fd1 --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Controls/ReadyControl.js @@ -0,0 +1,126 @@ +/** + * Ready system: + * + * The ready mechanism protects the players from being assigned to a match with settings they didn't explicitly agree with. + * It shall be technically possible to start a networked game until all participating players formally agree with the chosen settings. + * + * Therefore assume the readystate from the user interface rather than trusting the server whether the current player is ready. + * The server may set readiness to false but not to true. + * + * The ReadyControl class stores the ready state of the current player and fires an event if the agreed settings changed. + */ +class ReadyControl +{ + constructor(netMessages, gameSettingsControl, startGameControl, playerAssignmentsControl) + { + this.startGameControl = startGameControl; + this.playerAssignmentsControl = playerAssignmentsControl; + + this.resetReadyHandlers = new Set(); + this.previousAssignments = {}; + + // This variable keeps track whether the local player is ready + // As part of cheat prevention, the server may set this to NotReady, but + // only the UI may set it to Ready or StayReady. + this.readyState = this.NotReady; + + netMessages.registerNetMessageHandler("ready", this.onReadyMessage.bind(this)); + gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this)); + playerAssignmentsControl.registerClientJoinHandler(this.onClientJoin.bind(this)); + playerAssignmentsControl.registerClientLeaveHandler(this.onClientLeave.bind(this)); + } + + registerResetReadyHandler(handler) + { + this.resetReadyHandlers.add(handler); + } + + onClientJoin(newGUID, newAssignments) + { + if (newAssignments[newGUID].player != -1) + this.resetReady(); + } + + onClientLeave(guid) + { + if (g_PlayerAssignments[guid].player != -1) + this.resetReady(); + } + + onReadyMessage(message) + { + let playerAssignment = g_PlayerAssignments[message.guid]; + if (playerAssignment) + { + playerAssignment.status = message.status; + this.playerAssignmentsControl.updatePlayerAssignments(); + } + } + + onPlayerAssignmentsChange() + { + // Don't let the host tell you that you're ready when you're not. + let playerAssignment = g_PlayerAssignments[Engine.GetPlayerGUID()]; + if (playerAssignment && playerAssignment.status > this.readyState) + playerAssignment.status = this.readyState; + + for (let guid in g_PlayerAssignments) + if (this.previousAssignments[guid] && + this.previousAssignments[guid].player != g_PlayerAssignments[guid].player) + { + this.resetReady(); + return; + } + } + + onGameAttributesBatchChange() + { + this.resetReady(); + } + + setReady(ready, sendMessage) + { + this.readyState = ready; + + if (sendMessage) + Engine.SendNetworkReady(ready); + + // Update GUI objects instantly if relevant settingchange was detected + let playerAssignment = g_PlayerAssignments[Engine.GetPlayerGUID()]; + if (playerAssignment) + { + playerAssignment.status = ready; + this.playerAssignmentsControl.updatePlayerAssignments(); + } + } + + resetReady() + { + // The gameStarted check is only necessary to allow the host to + // determine the Seed and random items after clicking start + if (!g_IsNetworked || this.startGameControl.gameStarted) + return; + + for (let handler of this.resetReadyHandlers) + handler(); + + if (g_IsController) + { + Engine.ClearAllPlayerReady(); + this.playerAssignmentsControl.updatePlayerAssignments(); + } + else if (this.readyState != this.StayReady) + this.setReady(this.NotReady, false); + } + + getLocalReadyState() + { + return this.readyState; + } +} + +ReadyControl.prototype.NotReady = 0; + +ReadyControl.prototype.Ready = 1; + +ReadyControl.prototype.StayReady = 2; diff --git a/binaries/data/mods/public/gui/gamesetup/Controls/StartGameControl.js b/binaries/data/mods/public/gui/gamesetup/Controls/StartGameControl.js new file mode 100644 index 0000000000..ce8b196a4a --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Controls/StartGameControl.js @@ -0,0 +1,53 @@ +/** + * Cheat prevention: + * + * 1. Ensure that the host cannot start the game unless all clients agreed on the gamesettings using the ready system. + * + * TODO: + * 2. Ensure that the host cannot start the game with GameAttributes different from the agreed ones. + * This may be achieved by: + * - Determining the seed collectively. + * - passing the agreed gamesettings to the engine when starting the game instance + * - rejecting new gamesettings from the server after the game launch event + */ +class StartGameControl +{ + constructor(netMessages) + { + this.gameLaunchHandlers = new Set(); + + // This may be read from publicly + this.gameStarted = false; + + netMessages.registerNetMessageHandler("start", this.switchToLoadingPage.bind(this)); + } + + registerLaunchGameHandler(handler) + { + this.gameLaunchHandlers.add(handler); + } + + launchGame() + { + this.gameStarted = true; + + for (let handler of this.gameLaunchHandlers) + handler(); + + if (g_IsNetworked) + Engine.StartNetworkGame(); + else + { + Engine.StartGame(g_GameAttributes, g_PlayerAssignments.local.player); + this.switchToLoadingPage(); + } + } + + switchToLoadingPage() + { + Engine.SwitchGuiPage("page_loading.xml", { + "attribs": g_GameAttributes, + "playerAssignments": g_PlayerAssignments + }); + } +} diff --git a/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControl.js b/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControl.js new file mode 100644 index 0000000000..88d844233e --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControl.js @@ -0,0 +1,178 @@ +/** + * The GameSettingControl is an abstract class that is inherited by gamesetting control classes specific to a GUI object type, + * such as the GameSettingControlCheckbox or GameSettingControlDropdown. + * + * These classes are abstract classes too and are implemented by each handler class specific to one logical setting of g_GameAttributes. + * The purpose of these classes is to control precisely one logical setting of g_GameAttributes. + * Having one class per logical setting allows to handle each setting without making a restriction as to how the property should be written to g_GameAttributes or g_PlayerAssignments. + * The base classes allow implementing that while avoiding duplication. + * + * A GameSettingControl may depend on and read from other g_GameAttribute values, + * but the class instance is to be the sole instance writing to its setting value in g_GameAttributes and + * shall not write to setting values of other logical settings. + * + * The derived classes shall not make assumptions on the validity of g_GameAttributes, + * sanitize or delete their value if it is incompatible. + * + * A class should only write values to g_GameAttributes that it itself has confirmed to be accurate. + * This means that handlers may not copy an entire object or array of values, for example on mapchange. + * This avoids writing a setting value to g_GameAttributes that is not tracked and deleted when it becomes invalid. + * + * Since GameSettingControls shall be able to subscribe to g_GameAttributes changes, + * it is an obligation of the derived GameSettingControl class to broadcast the GameAttributesChange event each time it changes g_GameAttributes. + */ +class GameSettingControl +{ + // The constructor and inherited constructors shall not modify game attributes, + // since all GameSettingControl shall be able to subscribe to any gamesetting change. + constructor(gameSettingControlManager, category, playerIndex, gamesetupPage, gameSettingsControl, mapCache, mapFilters, netMessages, playerAssignmentsControl) + { + // Store arguments + { + this.category = category; + if (playerIndex !== undefined) + this.playerIndex = playerIndex; + this.gamesetupPage = gamesetupPage; + this.gameSettingsControl = gameSettingsControl; + this.mapCache = mapCache; + this.mapFilters = mapFilters; + this.netMessages = netMessages; + this.playerAssignmentsControl = playerAssignmentsControl; + } + + // enabled and hidden should only be modified through their setters or + // by calling updateVisibility after modification. + this.enabled = true; + this.hidden = false; + + if (this.setControl) + this.setControl(gameSettingControlManager); + + // This variable also used for autocompleting chat. + this.autocompleteTitle = undefined; + + if (this.title && this.TitleCaption) + this.setTitle(this.TitleCaption); + + if (this.Tooltip) + this.setTooltip(this.Tooltip); + + this.setHidden(false); + + if (this.onMapChange) + gameSettingsControl.registerMapChangeHandler(this.onMapChange.bind(this)); + + if (this.onLoad) + gamesetupPage.registerLoadHandler(this.onLoad.bind(this)); + + if (this.onGameAttributesChange) + gameSettingsControl.registerGameAttributesChangeHandler(this.onGameAttributesChange.bind(this)); + + if (this.onGameAttributesBatchChange) + gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this)); + + if (this.onAssignPlayer && this.playerIndex === 0) + this.gameSettingsControl.registerAssignPlayerHandler(this.onAssignPlayer.bind(this)); + + if (this.onPickRandomItems) + gameSettingsControl.registerPickRandomItemsHandler(this.onPickRandomItems.bind(this)); + + if (this.onGameAttributesFinalize) + gameSettingsControl.registerGameAttributesFinalizeHandler(this.onGameAttributesFinalize.bind(this)); + + if (this.onPlayerAssignmentsChange) + playerAssignmentsControl.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this)); + } + + setTitle(titleCaption) + { + this.autocompleteTitle = titleCaption; + this.title.caption = sprintf(this.TitleCaptionFormat, { + "setting": titleCaption + }); + } + + setTooltip(tooltip) + { + if (this.title) + this.title.tooltip = tooltip; + + if (this.label) + this.label.tooltip = tooltip; + + if (this.setControlTooltip) + this.setControlTooltip(tooltip); + } + + /** + * Do not call functions calling updateVisibility onMapChange but onGameAttributesChange, + * so that changes take effect when increasing the playercount as well. + */ + setEnabled(enabled) + { + this.enabled = enabled; + this.updateVisibility(); + } + + setHidden(hidden) + { + this.hidden = hidden; + this.updateVisibility(); + } + + updateVisibility() + { + let hidden = + this.hidden || + this.playerIndex === undefined && + this.category != g_TabCategorySelected || + this.playerIndex !== undefined && + g_GameAttributes.settings && this.playerIndex >= g_GameAttributes.settings.PlayerData.length; + + if (this.frame) + this.frame.hidden = hidden; + + if (hidden) + return; + + let enabled = g_IsController && this.enabled; + + if (this.setControlHidden) + this.setControlHidden(!enabled); + + if (this.label) + this.label.hidden = !!enabled; + } + + /** + * Returns whether the control specifies an order but didn't implement the function. + */ + addAutocompleteEntries(name, autocomplete) + { + if (this.autocompleteTitle) + autocomplete[0].push(this.autocompleteTitle); + + if (!Number.isInteger(this.AutocompleteOrder)) + return; + + if (!this.getAutocompleteEntries) + { + error(name + " specifies AutocompleteOrder but didn't implement getAutocompleteEntries"); + return; + } + + let newEntries = this.getAutocompleteEntries(); + if (newEntries) + autocomplete[this.AutocompleteOrder] = + (autocomplete[this.AutocompleteOrder] || []).concat(newEntries); + } +} + +GameSettingControl.prototype.TitleCaptionFormat = + translateWithContext("Title for specific setting", "%(setting)s:"); + +/** + * Derived classes can set this to a number to enable chat autocompleting of setting values. + * Higher numbers are autocompleted first. + */ +GameSettingControl.prototype.AutocompleteOrder = undefined; diff --git a/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlCheckbox.js b/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlCheckbox.js new file mode 100644 index 0000000000..cfc7a7f254 --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlCheckbox.js @@ -0,0 +1,60 @@ +/** + * This class is implemented by gamesettings that are controlled by a checkbox. + */ +class GameSettingControlCheckbox extends GameSettingControl +{ + constructor(...args) + { + super(...args); + + this.isInGuiUpdate = false; + this.previousSelectedValue = undefined; + } + + setControl(gameSettingControlManager) + { + let row = gameSettingControlManager.getNextRow("checkboxSettingFrame"); + this.frame = Engine.GetGUIObjectByName("checkboxSettingFrame[" + row + "]"); + this.checkbox = Engine.GetGUIObjectByName("checkboxSettingControl[" + row + "]"); + this.checkbox.onPress = this.onPressSuper.bind(this); + + let labels = this.frame.children[0].children; + this.title = labels[0]; + this.label = labels[1]; + } + + setControlTooltip(tooltip) + { + this.checkbox.tooltip = tooltip; + } + + setControlHidden(hidden) + { + this.checkbox.hidden = hidden; + } + + setChecked(checked) + { + if (this.previousSelectedValue == checked) + return; + + this.isInGuiUpdate = true; + this.checkbox.checked = checked; + this.isInGuiUpdate = false; + + if (this.label) + this.label.caption = checked ? this.Checked : this.Unchecked; + } + + onPressSuper() + { + if (!this.isInGuiUpdate) + this.onPress(this.checkbox.checked); + } +} + +GameSettingControlCheckbox.prototype.Checked = + translate("Yes"); + +GameSettingControlCheckbox.prototype.Unchecked = + translate("No"); diff --git a/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlCheckbox.xml b/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlCheckbox.xml new file mode 100644 index 0000000000..59ac7cde9b --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/GameSettings/GameSettingControlCheckbox.xml @@ -0,0 +1,14 @@ + +