Index: ps/trunk/binaries/data/mods/public/gui/options/options.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 24227) +++ ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 24228) @@ -1,624 +1,605 @@ [ { "label": "General", "options": [ { "type": "string", "label": "Player name (single-player)", "tooltip": "How you want to be addressed in single-player matches.", "config": "playername.singleplayer" }, { "type": "string", "label": "Player name (multiplayer)", "tooltip": "How you want to be addressed in multiplayer matches (except lobby).", "config": "playername.multiplayer" }, { "type": "boolean", "label": "Background pause", "tooltip": "Pause single-player games when window loses focus.", "config": "pauseonfocusloss" }, { "type": "boolean", "label": "Enable welcome screen", "tooltip": "If you disable it, the welcome screen will still appear once, each time a new version is available. You can always launch it from the main menu.", "config": "gui.splashscreen.enable" }, { "type": "boolean", "label": "Network warnings", "tooltip": "Show which player has a bad connection in multiplayer games.", "config": "overlay.netwarnings" }, { "type": "boolean", "label": "FPS overlay", "tooltip": "Show frames per second in top right corner.", "config": "overlay.fps" }, { "type": "boolean", "label": "Real time overlay", "tooltip": "Show current system time in top right corner.", "config": "overlay.realtime" }, { "type": "boolean", "label": "Game time overlay", "tooltip": "Show current simulation time in top right corner.", "config": "gui.session.timeelapsedcounter" }, { "type": "boolean", "label": "Ceasefire time overlay", "tooltip": "Always show the remaining ceasefire time.", "config": "gui.session.ceasefirecounter" }, { "type": "dropdown", "label": "Late observer joins", "tooltip": "Allow everybody or buddies only to join the game as observer after it started.", "config": "network.lateobservers", "list": [ { "value": "everyone", "label": "Everyone" }, { "value": "buddies", "label": "Buddies" }, { "value": "disabled", "label": "Disabled" } ] }, { "type": "number", "label": "Observer limit", "tooltip": "Prevent further observers from joining if the limit is reached.", "config": "network.observerlimit", "min": 0, "max": 32 }, { "type": "boolean", "label": "Chat timestamp", "tooltip": "Show time that messages are posted in the lobby, gamesetup and ingame chat.", "config": "chat.timestamp" } ] }, { "label": "Graphics", "tooltip": "Set the balance between performance and visual appearance.", "options": [ { "type": "boolean", "label": "Windowed mode", "tooltip": "Start 0 A.D. in a window.", "config": "windowed" }, { "type": "boolean", "label": "Prefer GLSL", "tooltip": "Use OpenGL 2.0 shaders (recommended).", - "config": "preferglsl", - "function": "Renderer_SetPreferGLSLEnabled" + "config": "preferglsl" }, { "type": "boolean", "label": "Fog", "tooltip": "Enable Fog.", "dependencies": ["preferglsl"], - "config": "fog", - "function": "Renderer_SetFogEnabled" + "config": "fog" }, { "type": "boolean", "label": "Post-processing", "tooltip": "Use screen-space post-processing filters (HDR, Bloom, DOF, etc).", - "config": "postproc", - "function": "Renderer_SetPostProcEnabled" + "config": "postproc" }, { "type": "dropdown", "label": "Anti-Aliasing", "tooltip": "Reduce aliasing effect on edges.", "dependencies": ["postproc", "preferglsl"], "config": "antialiasing", "list": [ { "value": "disabled", "label": "Disabled", "tooltip": "Do not use anti-aliasing." }, { "value": "fxaa", "label": "FXAA", "tooltip": "Fast, but simple anti-aliasing." }, { "value": "msaa2", "label": "MSAA (x2)", "tooltip": "Slow, but high-quality anti-aliasing, uses 2 samples per pixel. Supported for GL3.3+." }, { "value": "msaa4", "label": "MSAA (x4)", "tooltip": "Slow, but high-quality anti-aliasing, uses 4 samples per pixel. Supported for GL3.3+." }, { "value": "msaa8", "label": "MSAA (x8)", "tooltip": "Slow, but high-quality anti-aliasing, uses 8 samples per pixel. Supported for GL3.3+." }, { "value": "msaa16", "label": "MSAA (x16)", "tooltip": "Slow, but high-quality anti-aliasing, uses 16 samples per pixel. Supported for GL3.3+." } - ], - "function": "Renderer_UpdateAntiAliasingTechnique" + ] }, { "type": "dropdown", "label": "Sharpening", "tooltip": "Reduce blurry effects.", "dependencies": ["postproc", "preferglsl"], "config": "sharpening", "list": [ { "value": "disabled", "label": "Disabled", "tooltip": "Do not use sharpening." }, { "value": "cas", "label": "FidelityFX CAS", "tooltip": "Contrast adaptive sharpening, a fast, contrast based sharpening pass." } - ], - "function": "Renderer_UpdateSharpeningTechnique" + ] }, { "type": "slider", "label": "Sharpness factor", "tooltip": "The sharpness of the choosen pass.", "dependencies": ["postproc", "preferglsl"], "config": "sharpness", "min": 0, - "max": 1, - "function": "Renderer_UpdateSharpnessFactor" + "max": 1 }, { "type": "slider", "label": "Shader effects", "tooltip": "Number of shader effects. REQUIRES GAME RESTART", "config": "materialmgr.quality", "min": 0, "max": 10 }, { "type": "boolean", "label": "Shadows", "tooltip": "Enable shadows.", - "config": "shadows", - "function": "Renderer_SetShadowsEnabled" + "config": "shadows" }, { "type": "dropdown", "label": "Shadow quality", "tooltip": "Shadow map resolution. High values can crash the game when using a graphics card with low memory!", "dependencies": ["shadows"], "config": "shadowquality", - "function": "Renderer_RecreateShadowMap", "list": [ { "value": -2, "label": "Very Low" }, { "value": -1, "label": "Low" }, { "value": 0, "label": "Medium" }, { "value": 1, "label": "High" }, { "value": 2, "label": "Very High" } ] }, { "type": "boolean", "label": "Shadow filtering", "tooltip": "Smooth shadows.", "dependencies": ["shadows"], - "config": "shadowpcf", - "function": "Renderer_SetShadowPCFEnabled" + "config": "shadowpcf" }, { "type": "boolean", "label": "Unit silhouettes", "tooltip": "Show outlines of units behind structures.", - "config": "silhouettes", - "function": "Renderer_SetSilhouettesEnabled" + "config": "silhouettes" }, { "type": "boolean", "label": "Particles", "tooltip": "Enable particles.", - "config": "particles", - "function": "Renderer_SetParticlesEnabled" + "config": "particles" }, { "type": "boolean", "label": "Water effects", "tooltip": "When OFF, use the lowest settings possible to render water. This makes other settings irrelevant.", - "config": "watereffects", - "function": "Renderer_SetWaterEffectsEnabled" + "config": "watereffects" }, { "type": "boolean", "label": "High-quality water effects", "tooltip": "Use higher-quality effects for water, rendering coastal waves, shore foam, and ships trails.", "dependencies": ["watereffects"], - "config": "waterfancyeffects", - "function": "Renderer_SetWaterFancyEffectsEnabled" + "config": "waterfancyeffects" }, { "type": "boolean", "label": "Water reflections", "tooltip": "Allow water to reflect a mirror image.", "dependencies": ["watereffects"], - "config": "waterreflection", - "function": "Renderer_SetWaterReflectionEnabled" + "config": "waterreflection" }, { "type": "boolean", "label": "Water refraction", "tooltip": "Use a real water refraction map and not transparency.", "dependencies": ["watereffects"], - "config": "waterrefraction", - "function": "Renderer_SetWaterRefractionEnabled" + "config": "waterrefraction" }, { "type": "boolean", "label": "Real water depth", "tooltip": "Use actual water depth in rendering calculations.", "dependencies": ["watereffects", "waterrefraction"], - "config": "waterrealdepth", - "function": "Renderer_SetWaterRealDepthEnabled" + "config": "waterrealdepth" }, { "type": "boolean", "label": "Shadows on water", "tooltip": "Cast shadows on water.", "dependencies": ["watereffects"], - "config": "shadowsonwater", - "function": "Renderer_SetWaterShadowsEnabled" + "config": "shadowsonwater" }, { "type": "boolean", "label": "Smooth vision", "tooltip": "Lift darkness and fog-of-war smoothly.", - "config": "smoothlos", - "function": "Renderer_SetSmoothLOSEnabled" + "config": "smoothlos" }, { "type": "boolean", "label": "Show sky", "tooltip": "Render Sky.", - "config": "showsky", - "function": "Renderer_SetShowSkyEnabled" + "config": "showsky" }, { "type": "boolean", "label": "VSync", "tooltip": "Run vertical sync to fix screen tearing. REQUIRES GAME RESTART", "config": "vsync" }, { "type": "slider", "label": "FPS throttling in menus", "tooltip": "To save CPU workload, throttle render frequency in all menus. Set to maximum to disable throttling.", "config": "adaptivefps.menu", "min": 20, "max": 100 }, { "type": "slider", "label": "FPS throttling in games", "tooltip": "To save CPU workload, throttle render frequency in running games. Set to maximum to disable throttling.", "config": "adaptivefps.session", "min": 20, "max": 100 } ] }, { "label": "Sound", "options": [ { "type": "slider", "label": "Master volume", "tooltip": "Master audio gain.", "config": "sound.mastergain", "function": "SetMasterGain", "min": 0, "max": 2 }, { "type": "slider", "label": "Music volume", "tooltip": "In game music gain.", "config": "sound.musicgain", "function": "SetMusicGain", "min": 0, "max": 2 }, { "type": "slider", "label": "Ambient volume", "tooltip": "In game ambient sound gain.", "config": "sound.ambientgain", "function": "SetAmbientGain", "min": 0, "max": 2 }, { "type": "slider", "label": "Action volume", "tooltip": "In game unit action sound gain.", "config": "sound.actiongain", "function": "SetActionGain", "min": 0, "max": 2 }, { "type": "slider", "label": "UI volume", "tooltip": "UI sound gain.", "config": "sound.uigain", "function": "SetUIGain", "min": 0, "max": 2 }, { "type": "boolean", "label": "Nick notification", "tooltip": "Receive audio notification when someone types your nick.", "config": "sound.notify.nick" }, { "type": "boolean", "label": "Game setup - new player notification", "tooltip": "Receive audio notification when a new client joins the game setup.", "config": "sound.notify.gamesetup.join" } ] }, { "label": "Game Setup", "options": [ { "type": "boolean", "label": "Enable game setting tips", "tooltip": "Show tips when setting up a game.", "config": "gui.gamesetup.enabletips" }, { "type": "boolean", "label": "Enable settings panel slide", "tooltip": "Slide the settings panel when opening, closing or resizing.", "config": "gui.gamesetup.settingsslide" }, { "type": "boolean", "label": "Persist match settings", "tooltip": "Save and restore match settings for quick reuse when hosting another game.", "config": "persistmatchsettings" }, { "type": "dropdown", "label": "Default AI difficulty", "tooltip": "Default difficulty of the AI.", "config": "gui.gamesetup.aidifficulty", "list": [ { "value": 0, "label": "Sandbox" }, { "value": 1, "label": "Very Easy" }, { "value": 2, "label": "Easy" }, { "value": 3, "label": "Medium" }, { "value": 4, "label": "Hard" }, { "value": 5, "label": "Very Hard" } ] }, { "type": "dropdown", "label": "Default AI behavior", "tooltip": "Default behavior of the AI.", "config": "gui.gamesetup.aibehavior", "list": [ { "value": "random", "label": "Random" }, { "value": "balanced", "label": "Balanced" }, { "value": "aggressive", "label": "Aggressive" }, { "value": "defensive", "label": "Defensive" } ] }, { "type": "dropdown", "label": "Assign players", "tooltip": "Automatically assign joining clients to free player slots during the match setup.", "config": "gui.gamesetup.assignplayers", "list": [ { "value": "everyone", "label": "Everyone", "tooltip": "Players joining the match will be assigned if there is a free slot." }, { "value": "buddies", "label": "Buddies", "tooltip": "Players joining the match will only be assigned if they are a buddy of the host and if there is a free slot." }, { "value": "disabled", "label": "Disabled", "tooltip": "Players only receive a slot when the host assigns them explicitly." } ] } ] }, { "label": "Lobby", "tooltip": "These settings only affect the multiplayer.", "options": [ { "type": "boolean", "label": "TLS encryption", "tooltip": "Protect login and data exchanged with the lobby server using TLS encryption.", "config": "lobby.tls" }, { "type": "number", "label": "Chat backlog", "tooltip": "Number of backlogged messages to load when joining the lobby.", "config": "lobby.history", "min": "0" }, { "type": "boolean", "label": "Game rating column", "tooltip": "Show the average rating of the participating players in a column of the gamelist.", "config": "lobby.columns.gamerating" } ] }, { "label": "In-Game", "tooltip": "Change options regarding the in-game settings.", "options": [ { "type": "slider", "label": "Wounded unit health", "tooltip": "The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number.", "config": "gui.session.woundedunithotkeythreshold", "min": 0, "max": 100 }, { "type": "number", "label": "Batch training size", "tooltip": "Number of units trained per batch by default.", "config": "gui.session.batchtrainingsize", "min": 1, "max": 20 }, { "type": "slider", "label": "Scroll batch increment ratio", "tooltip": "Number of times you have to scroll to increase/decrease the batchsize by 1.", "config": "gui.session.scrollbatchratio", "min": 0.1, "max": 30 }, { "type": "boolean", "label": "Chat notification attack", "tooltip": "Show a chat notification if you are attacked by another player.", "config": "gui.session.notifications.attack" }, { "type": "boolean", "label": "Chat notification tribute", "tooltip": "Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode.", "config": "gui.session.notifications.tribute" }, { "type": "boolean", "label": "Chat notification barter", "tooltip": "Show a chat notification to observers when a player bartered resources.", "config": "gui.session.notifications.barter" }, { "type": "dropdown", "label": "Chat notification phase", "tooltip": "Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode.", "config": "gui.session.notifications.phase", "list": [ { "value": "none", "label": "Disable" }, { "value": "completed", "label": "Completed" }, { "value": "all", "label": "All displayed" } ] }, { "type": "boolean", "label": "Attack range visualization", "tooltip": "Display the attack range of selected defensive structures (can also be toggled in-game with the hotkey).", "config": "gui.session.attackrange" }, { "type": "boolean", "label": "Aura range visualization", "tooltip": "Display the range of auras of selected units and structures (can also be toggled in-game with the hotkey).", "config": "gui.session.aurasrange" }, { "type": "boolean", "label": "Heal range visualization", "tooltip": "Display the healing range of selected units (can also be toggled in-game with the hotkey).", "config": "gui.session.healrange" }, { "type": "boolean", "label": "Rank icon above status bar", "tooltip": "Show rank icons above status bars.", "config": "gui.session.rankabovestatusbar" }, { "type": "boolean", "label": "Experience status bar", "tooltip": "Show an experience status bar above each selected unit.", "config": "gui.session.experiencestatusbar" }, { "type": "boolean", "label": "Detailed tooltips", "tooltip": "Show detailed tooltips for trainable units in unit-producing structures.", "config": "showdetailedtooltips" }, { "type": "dropdown", "label": "Sort resources and population tooltip", "tooltip": "Dynamically sort players in the resources and population tooltip by value.", "config": "gui.session.respoptooltipsort", "list": [ { "value": 0, "label": "Unordered" }, { "value": -1, "label": "Ascending" }, { "value": 1, "label": "Descending" } ] }, { "type": "color", "label": "Diplomacy colors: self", "tooltip": "Color of your units when diplomacy colors are enabled.", "config": "gui.session.diplomacycolors.self" }, { "type": "color", "label": "Diplomacy colors: ally", "tooltip": "Color of allies when diplomacy colors are enabled.", "config": "gui.session.diplomacycolors.ally" }, { "type": "color", "label": "Diplomacy colors: neutral", "tooltip": "Color of neutral players when diplomacy colors are enabled.", "config": "gui.session.diplomacycolors.neutral" }, { "type": "color", "label": "Diplomacy colors: enemy", "tooltip": "Color of enemies when diplomacy colors are enabled.", "config": "gui.session.diplomacycolors.enemy" }, { "type": "dropdown", "label": "Snap to edges", "tooltip": "This option allows to align new structures with nearby structures.", "config": "gui.session.snaptoedges", "list": [ { "value": "disabled", "label": "Hotkey to enable snapping", "tooltip": "New structures are aligned with nearby structures while pressing the hotkey." }, { "value": "enabled", "label": "Hotkey to disable snapping", "tooltip": "New structures are aligned with nearby structures unless the hotkey is pressed." } ] }, { "type": "dropdown", "label": "Control Group Membership", "tooltip": "Decide whether units can be part of multiple control groups.", "config": "gui.session.disjointcontrolgroups", "list": [ { "value": "true", "label": "Single", "tooltip": "When adding a Unit or Structure to a control group, they are removed from other control groups. Use this choice if you want control groups to refer to distinct armies." }, { "value": "false", "label": "Multiple", "tooltip": "Units and Structures can be part of multiple control groups. This is useful to keep control groups for distinct armies and a control group for the entire army simultaneously." } ] } ] } ] Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 24227) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 24228) @@ -1,813 +1,813 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_CivData = loadCivData(false, true); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; var g_Ambient; var g_Chat; var g_Cheats; var g_DeveloperOverlay; var g_DiplomacyColors; var g_DiplomacyDialog; var g_GameSpeedControl; var g_Menu; var g_MiniMapPanel; var g_NetworkStatusOverlay; var g_ObjectivesDialog; var g_OutOfSyncNetwork; var g_OutOfSyncReplay; var g_PanelEntityManager; var g_PauseControl; var g_PauseOverlay; var g_PlayerViewControl; var g_QuitConfirmationDefeat; var g_QuitConfirmationReplay; var g_RangeOverlayManager; var g_ResearchProgress; var g_TimeNotificationOverlay; var g_TopPanel; var g_TradeDialog; /** * Map, player and match settings set in gamesetup. */ const g_GameAttributes = deepfreeze(Engine.GuiInterfaceCall("GetInitAttributes")); /** * True if this is a multiplayer game. */ const g_IsNetworked = Engine.HasNetClient(); /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ var g_IsController = !g_IsNetworked || Engine.HasNetServer(); /** * Whether we have finished the synchronization and * can start showing simulation related message boxes. */ var g_IsNetworkedActive = false; /** * True if the connection to the server has been lost. */ var g_Disconnected = false; /** * True if the current user has observer capabilities. */ var g_IsObserver = false; /** * True if the current user has rejoined (or joined the game after it started). */ var g_HasRejoined = false; /** * The playerID selected in the change perspective tool. */ var g_ViewedPlayer = Engine.GetPlayerID(); /** * True if the camera should focus on attacks and player commands * and select the affected units. */ var g_FollowPlayer = false; /** * Cache the basic player data (name, civ, color). */ var g_Players = []; /** * Last time when onTick was called(). * Used for animating the main menu. */ var g_LastTickTime = Date.now(); /** * Recalculate which units have their status bars shown with this frequency in milliseconds. */ var g_StatusBarUpdate = 200; /** * For restoring selection, order and filters when returning to the replay menu */ var g_ReplaySelectionData; /** * Remembers which clients are assigned to which player slots. * The keys are GUIDs or "local" in single-player. */ var g_PlayerAssignments; /** * Whether the entire UI should be hidden (useful for promotional screenshots). * Can be toggled with a hotkey. */ var g_ShowGUI = true; /** * Whether status bars should be shown for all of the player's units. */ var g_ShowAllStatusBars = false; /** * Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update). */ var g_SimState; var g_EntityStates = {}; var g_TemplateData = {}; var g_TechnologyData = {}; var g_ResourceData = new Resources(); /** * These handlers are called each time a new turn was simulated. * Use this as sparely as possible. */ var g_SimulationUpdateHandlers = new Set(); /** * These handlers are called after the player states have been initialized. */ var g_PlayersInitHandlers = new Set(); /** * These handlers are called when a player has been defeated or won the game. */ var g_PlayerFinishedHandlers = new Set(); /** * These events are fired whenever the player added or removed entities from the selection. */ var g_EntitySelectionChangeHandlers = new Set(); /** * These events are fired when the user has performed a hotkey assignment change. * Currently only fired on init, but to be fired from any hotkey editor dialog. */ var g_HotkeyChangeHandlers = new Set(); /** * List of additional entities to highlight. */ var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; /** * Order in which the panel entities are shown. */ var g_PanelEntityOrder = ["Hero", "Relic"]; /** * Unit classes to be checked for the idle-worker-hotkey. */ var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "Citizen"]; /** * Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey. */ var g_MilitaryTypes = ["Melee", "Ranged"]; function GetSimState() { if (!g_SimState) g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState")); return g_SimState; } function GetMultipleEntityStates(ents) { if (!ents.length) return null; let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents); for (let item of entityStates) g_EntityStates[item.entId] = item.state && deepfreeze(item.state); return entityStates; } function GetEntityState(entId) { if (!g_EntityStates[entId]) { let entityState = Engine.GuiInterfaceCall("GetEntityState", entId); g_EntityStates[entId] = entityState && deepfreeze(entityState); } return g_EntityStates[entId]; } /** * Returns template data calling GetTemplateData defined in GuiInterface.js * and deepfreezing returned object. * @param {string} templateName - Data of this template will be returned. * @param {number|undefined} player - Modifications of this player will be applied to the template. * If undefined, id of player calling this method will be used. */ function GetTemplateData(templateName, player) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": player }); translateObjectKeys(template, ["specific", "generic", "tooltip"]); g_TemplateData[templateName] = deepfreeze(template); } return g_TemplateData[templateName]; } function GetTechnologyData(technologyName, civ) { if (!g_TechnologyData[civ]) g_TechnologyData[civ] = {}; if (!(technologyName in g_TechnologyData[civ])) { let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), civ, g_ResourceData); translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); g_TechnologyData[civ][technologyName] = deepfreeze(template); } return g_TechnologyData[civ][technologyName]; } function init(initData, hotloadData) { if (!g_Settings) { Engine.EndGame(); Engine.SwitchGuiPage("page_pregame.xml"); return; } // Fallback used by atlas g_PlayerAssignments = initData ? initData.playerAssignments : { "local": { "player": 1 } }; // Fallback used by atlas and autostart games if (g_PlayerAssignments.local && !g_PlayerAssignments.local.name) g_PlayerAssignments.local.name = singleplayerName(); if (initData) { g_ReplaySelectionData = initData.replaySelectionData; g_HasRejoined = initData.isRejoining; if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); } let mapCache = new MapCache(); g_Cheats = new Cheats(); g_DiplomacyColors = new DiplomacyColors(); g_PlayerViewControl = new PlayerViewControl(); g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects); g_PauseControl = new PauseControl(); g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay); g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates); g_Ambient = new Ambient(); g_Chat = new Chat(g_PlayerViewControl, g_Cheats); g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection); g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors); g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl); g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat); g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes); g_NetworkStatusOverlay = new NetworkStatusOverlay(); g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl, mapCache); g_OutOfSyncNetwork = new OutOfSyncNetwork(); g_OutOfSyncReplay = new OutOfSyncReplay(); g_PanelEntityManager = new PanelEntityManager(g_PlayerViewControl, g_Selection, g_PanelEntityOrder); g_PauseOverlay = new PauseOverlay(g_PauseControl); g_QuitConfirmationDefeat = new QuitConfirmationDefeat(); g_QuitConfirmationReplay = new QuitConfirmationReplay(); g_RangeOverlayManager = new RangeOverlayManager(g_Selection); g_ResearchProgress = new ResearchProgress(g_PlayerViewControl, g_Selection); g_TradeDialog = new TradeDialog(g_PlayerViewControl); g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl); g_TimeNotificationOverlay = new TimeNotificationOverlay(g_PlayerViewControl); initBatchTrain(); initSelectionPanels(); LoadModificationTemplates(); updatePlayerData(); initializeMusic(); // before changing the perspective Engine.SetBoundingBoxDebugOverlay(false); for (let handler of g_PlayersInitHandlers) handler(); for (let handler of g_HotkeyChangeHandlers) handler(); if (hotloadData) { g_Selection.selected = hotloadData.selection; g_PlayerAssignments = hotloadData.playerAssignments; g_Players = hotloadData.player; } // TODO: use event instead onSimulationUpdate(); setTimeout(displayGamestateNotifications, 1000); } function registerPlayersInitHandler(handler) { g_PlayersInitHandlers.add(handler); } function registerPlayersFinishedHandler(handler) { g_PlayerFinishedHandlers.add(handler); } function registerSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.add(handler); } function unregisterSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.delete(handler); } function registerEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.add(handler); } function unregisterEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.delete(handler); } function registerHotkeyChangeHandler(handler) { g_HotkeyChangeHandlers.add(handler); } function updatePlayerData() { let simState = GetSimState(); if (!simState) return; let playerData = []; for (let i = 0; i < simState.players.length; ++i) { let playerState = simState.players[i]; playerData.push({ "name": playerState.name, "civ": playerState.civ, "color": { "r": playerState.color.r * 255, "g": playerState.color.g * 255, "b": playerState.color.b * 255, "a": playerState.color.a * 255 }, "team": playerState.team, "teamsLocked": playerState.teamsLocked, "cheatsEnabled": playerState.cheatsEnabled, "state": playerState.state, "isAlly": playerState.isAlly, "isMutualAlly": playerState.isMutualAlly, "isNeutral": playerState.isNeutral, "isEnemy": playerState.isEnemy, "guid": undefined, // network guid for players controlled by hosts "offline": g_Players[i] && !!g_Players[i].offline }); } for (let guid in g_PlayerAssignments) { let playerID = g_PlayerAssignments[guid].player; if (!playerData[playerID]) continue; playerData[playerID].guid = guid; playerData[playerID].name = g_PlayerAssignments[guid].name; } g_Players = playerData; } /** * Returns the entity itself except when garrisoned where it returns its garrisonHolder */ function getEntityOrHolder(ent) { let entState = GetEntityState(ent); if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length && entState.unitAI.orders[0].type == "Garrison") return getEntityOrHolder(entState.unitAI.orders[0].data.target); return ent; } function initializeMusic() { initMusic(); if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music) global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music); global.music.setState(global.music.states.PEACE); } function resetTemplates() { // Update GUI and clear player-dependent cache g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); // TODO: do this more selectively onSimulationUpdate(); } /** * Returns true if the player with that ID is in observermode. */ function isPlayerObserver(playerID) { let playerStates = GetSimState().players; return !playerStates[playerID] || playerStates[playerID].state != "active"; } /** * Returns true if the current user can issue commands for that player. */ function controlsPlayer(playerID) { let playerStates = GetSimState().players; return !!playerStates[Engine.GetPlayerID()] && playerStates[Engine.GetPlayerID()].controlsAll || Engine.GetPlayerID() == playerID && !!playerStates[playerID] && playerStates[playerID].state != "defeated"; } /** * Called when one or more players have won or were defeated. * * @param {array} - IDs of the players who have won or were defeated. * @param {Object} - a plural string stating the victory reason. * @param {boolean} - whether these players have won or lost. */ function playersFinished(players, victoryString, won) { addChatMessage({ "type": "playerstate", "message": victoryString, "players": players }); updatePlayerData(); // TODO: The other calls in this function should move too for (let handler of g_PlayerFinishedHandlers) handler(players, won); if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning()) return; global.music.setState( won ? global.music.states.VICTORY : global.music.states.DEFEAT ); } function resumeGame() { g_PauseControl.implicitResume(); } function closeOpenDialogs() { g_Menu.close(); g_Chat.closePage(); g_DiplomacyDialog.close(); g_ObjectivesDialog.close(); g_TradeDialog.close(); } function endGame() { // Before ending the game let replayDirectory = Engine.GetCurrentReplayDirectory(); let simData = Engine.GuiInterfaceCall("GetReplayMetadata"); let playerID = Engine.GetPlayerID(); Engine.EndGame(); // After the replay file was closed in EndGame // Done here to keep EndGame small if (!g_IsReplay) Engine.AddReplayToCache(replayDirectory); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); Engine.SwitchGuiPage("page_summary.xml", { "sim": simData, "gui": { "dialog": false, "assignedPlayer": playerID, "disconnected": g_Disconnected, "isReplay": g_IsReplay, "replayDirectory": !g_HasRejoined && replayDirectory, "replaySelectionData": g_ReplaySelectionData } }); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { "selection": g_Selection.selected, "playerAssignments": g_PlayerAssignments, "player": g_Players, }; } function getSavedGameData() { return { "groups": g_Groups.groups }; } function restoreSavedGameData(data) { // Restore camera if any if (data.camera) Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ, data.camera.RotX, data.camera.RotY, data.camera.Zoom); // Clear selection when loading a game g_Selection.reset(); // Restore control groups for (let groupNumber in data.groups) { g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups; g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents; } updateGroups(); } /** * Called every frame. */ function onTick() { if (!g_Settings) return; let now = Date.now(); let tickLength = now - g_LastTickTime; g_LastTickTime = now; handleNetMessages(); updateCursorAndTooltip(); if (g_Selection.dirty) { g_Selection.dirty = false; // When selection changed, get the entityStates of new entities GetMultipleEntityStates(g_Selection.toList().filter(entId => !g_EntityStates[entId])); for (let handler of g_EntitySelectionChangeHandlers) handler(); updateGUIObjects(); // Display rally points for selected structures. if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength) recalculateStatusBarDisplay(); updateTimers(); Engine.GuiInterfaceCall("ClearRenamedEntities"); } function onSimulationUpdate() { // Templates change depending on technologies and auras, so they have to be reloaded after such a change. // g_TechnologyData data never changes, so it shouldn't be deleted. g_EntityStates = {}; if (Engine.GuiInterfaceCall("IsTemplateModified")) { g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); } g_SimState = undefined; // Some changes may require re-rendering the selection. if (Engine.GuiInterfaceCall("IsSelectionDirty")) { g_Selection.onChange(); Engine.GuiInterfaceCall("ResetSelectionDirty"); } if (!GetSimState()) return; GetMultipleEntityStates(g_Selection.toList()); for (let handler of g_SimulationUpdateHandlers) handler(); // TODO: Move to handlers updateCinemaPath(); handleNotifications(); updateGUIObjects(); } function toggleGUI() { g_ShowGUI = !g_ShowGUI; updateCinemaPath(); } function updateCinemaPath() { let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath; - Engine.Renderer_SetSilhouettesEnabled(!isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true"); + Engine.ConfigDB_CreateValue("user", "silhouettes", !isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true" ? "true" : "false"); } // TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updateGroups(); updateSelectionDetails(); updateBuildingPlacementPreview(); if (!g_IsObserver) { // Update music state on basis of battle state. let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer); if (battleState) global.music.setState(global.music.states[battleState]); } } function updateGroups() { g_Groups.update(); // Determine the sum of the costs of a given template let getCostSum = (ent) => { let cost = GetTemplateData(GetEntityState(ent).template).cost; return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0; }; for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children) { Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i; let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]"); button.hidden = g_Groups.groups[i].getTotalCount() == 0; button.onPress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i); button.onDoublePress = (function(i) { return function() { performGroup("snap", i); }; })(i); button.onPressRight = (function(i) { return function() { performGroup("breakUp", i); }; })(i); // Choose the icon of the most common template (or the most costly if it's not unique) if (g_Groups.groups[i].getTotalCount() > 0) { let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => { if (pre.ents.length == cur.ents.length) return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur; return pre.ents.length > cur.ents.length ? pre : cur; }).ents[0]).template).icon; Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite = icon ? ("stretched:session/portraits/" + icon) : "groupsIcon"; } setPanelObjectPosition(button, i, 1); } } /** * Toggles the display of status bars for all of the player's entities. * * @param {boolean} remove - Whether to hide all previously shown status bars. */ function recalculateStatusBarDisplay(remove = false) { let entities; if (g_ShowAllStatusBars && !remove) entities = g_ViewedPlayer == -1 ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer); else { let selected = g_Selection.toList(); for (let ent in g_Selection.highlighted) selected.push(g_Selection.highlighted[ent]); // Remove selected entities from the 'all entities' array, // to avoid disabling their status bars. entities = Engine.GuiInterfaceCall( g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", { "viewedPlayer": g_ViewedPlayer }).filter(idx => selected.indexOf(idx) == -1); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars && !remove, "showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true", "showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true" }); } function removeStatusBarDisplay() { if (g_ShowAllStatusBars) recalculateStatusBarDisplay(true); } /** * Inverts the given configuration boolean and returns the current state. * For example "silhouettes". */ function toggleConfigBool(configName) { let enabled = Engine.ConfigDB_GetValue("user", configName) != "true"; Engine.ConfigDB_CreateAndWriteValueToFile("user", configName, String(enabled), "config/user.cfg"); return enabled; } // Update the additional list of entities to be highlighted. function updateAdditionalHighlight() { let entsAdd = []; // list of entities units to be highlighted let entsRemove = []; let highlighted = g_Selection.toList(); for (let ent in g_Selection.highlighted) highlighted.push(g_Selection.highlighted[ent]); if (g_ShowGuarding) // flag the guarding entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.guard || !state.guard.entities.length) continue; for (let ent of state.guard.entities) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } if (g_ShowGuarded) // flag the guarded entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.unitAI || !state.unitAI.isGuarding) continue; let ent = state.unitAI.isGuarding; if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } // flag the entities to remove (from the previously added) from this additional highlight for (let ent of g_AdditionalHighlight) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1) entsRemove.push(ent); _setHighlight(entsAdd, g_HighlightedAlpha, true); _setHighlight(entsRemove, 0, false); g_AdditionalHighlight = entsAdd; } Index: ps/trunk/source/ps/ConfigDB.cpp =================================================================== --- ps/trunk/source/ps/ConfigDB.cpp (revision 24227) +++ ps/trunk/source/ps/ConfigDB.cpp (revision 24228) @@ -1,467 +1,490 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ConfigDB.h" #include #include "lib/allocators/shared_ptr.h" #include "lib/file/vfs/vfs_path.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Filesystem.h" #include #include typedef std::map TConfigMap; TConfigMap CConfigDB::m_Map[CFG_LAST]; VfsPath CConfigDB::m_ConfigFile[CFG_LAST]; bool CConfigDB::m_HasChanges[CFG_LAST]; +std::multimap> CConfigDB::m_Hooks; + +void TriggerAllHooks(const std::multimap>& hooks, const CStr& name) +{ + std::for_each(hooks.lower_bound(name), hooks.upper_bound(name), [](const std::pair>& hook) { hook.second(); }); +} + static std::recursive_mutex cfgdb_mutex; // These entries will not be printed to logfiles, so that logfiles can be shared without leaking personal or sensitive data static const std::unordered_set g_UnloggedEntries = { "lobby.password", "lobby.buddies", "userreport.id" // authentication token for GDPR personal data requests }; #define CHECK_NS(rval)\ do {\ if (ns < 0 || ns >= CFG_LAST)\ {\ debug_warn(L"CConfigDB: Invalid ns value");\ return rval;\ }\ } while (false) namespace { template void Get(const CStr& value, T& ret) { std::stringstream ss(value); ss >> ret; } template<> void Get<>(const CStr& value, bool& ret) { ret = value == "true"; } template<> void Get<>(const CStr& value, std::string& ret) { ret = value; } std::string EscapeString(const CStr& str) { std::string ret; for (size_t i = 0; i < str.length(); ++i) { if (str[i] == '\\') ret += "\\\\"; else if (str[i] == '"') ret += "\\\""; else ret += str[i]; } return ret; } } // namespace #define GETVAL(type)\ void CConfigDB::GetValue(EConfigNamespace ns, const CStr& name, type& value)\ {\ CHECK_NS(;);\ std::lock_guard s(cfgdb_mutex);\ TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name);\ if (it != m_Map[CFG_COMMAND].end())\ {\ Get(it->second[0], value);\ return;\ }\ for (int search_ns = ns; search_ns >= 0; --search_ns)\ {\ it = m_Map[search_ns].find(name);\ if (it != m_Map[search_ns].end())\ {\ Get(it->second[0], value);\ return;\ }\ }\ } GETVAL(bool) GETVAL(int) GETVAL(u32) GETVAL(float) GETVAL(double) GETVAL(std::string) #undef GETVAL bool CConfigDB::HasChanges(EConfigNamespace ns) const { CHECK_NS(false); std::lock_guard s(cfgdb_mutex); return m_HasChanges[ns]; } void CConfigDB::SetChanges(EConfigNamespace ns, bool value) { CHECK_NS(;); std::lock_guard s(cfgdb_mutex); m_HasChanges[ns] = value; } void CConfigDB::GetValues(EConfigNamespace ns, const CStr& name, CConfigValueSet& values) const { CHECK_NS(;); std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name); if (it != m_Map[CFG_COMMAND].end()) { values = it->second; return; } for (int search_ns = ns; search_ns >= 0; --search_ns) { it = m_Map[search_ns].find(name); if (it != m_Map[search_ns].end()) { values = it->second; return; } } } EConfigNamespace CConfigDB::GetValueNamespace(EConfigNamespace ns, const CStr& name) const { CHECK_NS(CFG_LAST); std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name); if (it != m_Map[CFG_COMMAND].end()) return CFG_COMMAND; for (int search_ns = ns; search_ns >= 0; --search_ns) { it = m_Map[search_ns].find(name); if (it != m_Map[search_ns].end()) return (EConfigNamespace)search_ns; } return CFG_LAST; } std::map CConfigDB::GetValuesWithPrefix(EConfigNamespace ns, const CStr& prefix) const { std::lock_guard s(cfgdb_mutex); std::map ret; CHECK_NS(ret); // Loop upwards so that values in later namespaces can override // values in earlier namespaces for (int search_ns = 0; search_ns <= ns; ++search_ns) for (const std::pair& p : m_Map[search_ns]) if (boost::algorithm::starts_with(p.first, prefix)) ret[p.first] = p.second; for (const std::pair& p : m_Map[CFG_COMMAND]) if (boost::algorithm::starts_with(p.first, prefix)) ret[p.first] = p.second; return ret; } void CConfigDB::SetValueString(EConfigNamespace ns, const CStr& name, const CStr& value) { CHECK_NS(;); std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[ns].find(name); if (it == m_Map[ns].end()) it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1))); it->second[0] = value; + + TriggerAllHooks(m_Hooks, name); } void CConfigDB::SetValueBool(EConfigNamespace ns, const CStr& name, const bool value) { CStr valueString = value ? "true" : "false"; SetValueString(ns, name, valueString); } void CConfigDB::SetValueList(EConfigNamespace ns, const CStr& name, std::vector values) { CHECK_NS(;); std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[ns].find(name); if (it == m_Map[ns].end()) it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1))); it->second = values; } void CConfigDB::RemoveValue(EConfigNamespace ns, const CStr& name) { CHECK_NS(;); std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[ns].find(name); if (it == m_Map[ns].end()) return; m_Map[ns].erase(it); + + TriggerAllHooks(m_Hooks, name); } void CConfigDB::SetConfigFile(EConfigNamespace ns, const VfsPath& path) { CHECK_NS(;); std::lock_guard s(cfgdb_mutex); m_ConfigFile[ns] = path; } bool CConfigDB::Reload(EConfigNamespace ns) { CHECK_NS(false); std::lock_guard s(cfgdb_mutex); shared_ptr buffer; size_t buflen; { // Handle missing files quietly if (g_VFS->GetFileInfo(m_ConfigFile[ns], NULL) < 0) { LOGMESSAGE("Cannot find config file \"%s\" - ignoring", m_ConfigFile[ns].string8()); return false; } LOGMESSAGE("Loading config file \"%s\"", m_ConfigFile[ns].string8()); Status ret = g_VFS->LoadFile(m_ConfigFile[ns], buffer, buflen); if (ret != INFO::OK) { LOGERROR("CConfigDB::Reload(): vfs_load for \"%s\" failed: return was %lld", m_ConfigFile[ns].string8(), (long long)ret); return false; } } TConfigMap newMap; char *filebuf = (char*)buffer.get(); char *filebufend = filebuf+buflen; bool quoted = false; CStr header; CStr name; CStr value; int line = 1; std::vector values; for (char* pos = filebuf; pos < filebufend; ++pos) { switch (*pos) { case '\n': case ';': break; // We finished parsing this line case ' ': case '\r': case '\t': continue; // ignore case '[': header.clear(); for (++pos; pos < filebufend && *pos != '\n' && *pos != ']'; ++pos) header.push_back(*pos); if (pos == filebufend || *pos == '\n') { LOGERROR("Config header with missing close tag encountered on line %d in '%s'", line, m_ConfigFile[ns].string8()); header.clear(); ++line; continue; } LOGMESSAGE("Found config header '%s'", header.c_str()); header.push_back('.'); while (++pos < filebufend && *pos != '\n' && *pos != ';') if (*pos != ' ' && *pos != '\r') { LOGERROR("Config settings on the same line as a header on line %d in '%s'", line, m_ConfigFile[ns].string8()); break; } while (pos < filebufend && *pos != '\n') ++pos; ++line; continue; case '=': // Parse parameters (comma separated, possibly quoted) for (++pos; pos < filebufend && *pos != '\n' && *pos != ';'; ++pos) { switch (*pos) { case '"': quoted = true; // parse until not quoted anymore for (++pos; pos < filebufend && *pos != '\n' && *pos != '"'; ++pos) { if (*pos == '\\' && ++pos == filebufend) { LOGERROR("Escape character at end of input (line %d in '%s')", line, m_ConfigFile[ns].string8()); break; } value.push_back(*pos); } if (pos < filebufend && *pos == '"') quoted = false; else --pos; // We should terminate the outer loop too break; case ' ': case '\r': case '\t': break; // ignore case ',': if (!value.empty()) values.push_back(value); value.clear(); break; default: value.push_back(*pos); break; } } if (quoted) // We ignore the invalid parameter LOGERROR("Unmatched quote while parsing config file '%s' on line %d", m_ConfigFile[ns].string8(), line); else if (!value.empty()) values.push_back(value); value.clear(); quoted = false; break; // We are either at the end of the line, or we still have a comment to parse default: name.push_back(*pos); continue; } // Consume the rest of the line while (pos < filebufend && *pos != '\n') ++pos; // Store the setting if (!name.empty() && !values.empty()) { CStr key(header + name); newMap[key] = values; if (g_UnloggedEntries.find(key) != g_UnloggedEntries.end()) LOGMESSAGE("Loaded config string \"%s\"", key); else { std::string vals; for (size_t i = 0; i < newMap[key].size() - 1; ++i) vals += "\"" + EscapeString(newMap[key][i]) + "\", "; vals += "\"" + EscapeString(newMap[key][values.size()-1]) + "\""; LOGMESSAGE("Loaded config string \"%s\" = %s", key, vals); } } else if (!name.empty()) LOGERROR("Encountered config setting '%s' without value while parsing '%s' on line %d", name, m_ConfigFile[ns].string8(), line); name.clear(); values.clear(); ++line; } if (!name.empty()) LOGERROR("Config file does not have a new line after the last config setting '%s'", name); m_Map[ns].swap(newMap); return true; } bool CConfigDB::WriteFile(EConfigNamespace ns) const { CHECK_NS(false); std::lock_guard s(cfgdb_mutex); return WriteFile(ns, m_ConfigFile[ns]); } bool CConfigDB::WriteFile(EConfigNamespace ns, const VfsPath& path) const { CHECK_NS(false); std::lock_guard s(cfgdb_mutex); shared_ptr buf; AllocateAligned(buf, 1*MiB, maxSectorSize); char* pos = (char*)buf.get(); for (const std::pair& p : m_Map[ns]) { size_t i; pos += sprintf(pos, "%s = ", p.first.c_str()); for (i = 0; i < p.second.size() - 1; ++i) pos += sprintf(pos, "\"%s\", ", EscapeString(p.second[i]).c_str()); pos += sprintf(pos, "\"%s\"\n", EscapeString(p.second[i]).c_str()); } const size_t len = pos - (char*)buf.get(); Status ret = g_VFS->CreateFile(path, buf, len); if (ret < 0) { LOGERROR("CConfigDB::WriteFile(): CreateFile \"%s\" failed (error: %d)", path.string8(), (int)ret); return false; } return true; } bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value) { CHECK_NS(false); std::lock_guard s(cfgdb_mutex); return WriteValueToFile(ns, name, value, m_ConfigFile[ns]); } bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value, const VfsPath& path) { CHECK_NS(false); std::lock_guard s(cfgdb_mutex); TConfigMap newMap; m_Map[ns].swap(newMap); Reload(ns); SetValueString(ns, name, value); bool ret = WriteFile(ns, path); m_Map[ns].swap(newMap); return ret; } +CConfigDB::hook_t CConfigDB::RegisterHookAndCall(const CStr& name, std::function hook) +{ + hook(); + return m_Hooks.emplace(name, hook); +} + +void CConfigDB::UnregisterHook(CConfigDB::hook_t&& hook) +{ + if (hook.ptr != m_Hooks.end()) + m_Hooks.erase(hook.ptr); +} + #undef CHECK_NS Index: ps/trunk/source/ps/ConfigDB.h =================================================================== --- ps/trunk/source/ps/ConfigDB.h (revision 24227) +++ ps/trunk/source/ps/ConfigDB.h (revision 24228) @@ -1,182 +1,208 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* CConfigDB - Load, access and store configuration variables TDD : http://www.wildfiregames.com/forum/index.php?showtopic=1125 OVERVIEW: JavaScript: Check this documentation: http://trac.wildfiregames.com/wiki/Exposed_ConfigDB_Functions */ #ifndef INCLUDED_CONFIGDB #define INCLUDED_CONFIGDB #include "lib/file/vfs/vfs_path.h" #include "ps/CStr.h" #include "ps/Singleton.h" #include /** * Namespace priorities: * - Command line args override everything * - User supersedes HWDetect (let the user try crashing his system). * - HWDetect supersedes mods & default -> mods can mod hwdetect itself. * - SYSTEM is used for local.cfg and is basically for setting custom defaults. */ enum EConfigNamespace { CFG_DEFAULT, CFG_MOD, CFG_SYSTEM, CFG_HWDETECT, CFG_USER, CFG_COMMAND, CFG_LAST }; -typedef std::vector CConfigValueSet; +using CConfigValueSet = std::vector; #define g_ConfigDB CConfigDB::GetSingleton() class CConfigDB : public Singleton { public: /** * Attempt to retrieve the value of a config variable with the given name; * will search CFG_COMMAND first, and then all namespaces from the specified * namespace down. */ void GetValue(EConfigNamespace ns, const CStr& name, bool& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, int& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, u32& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, float& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, double& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, std::string& value); /** * Returns true if changed with respect to last write on file */ bool HasChanges(EConfigNamespace ns) const; void SetChanges(EConfigNamespace ns, bool value); /** * Attempt to retrieve a vector of values corresponding to the given setting; * will search CFG_COMMAND first, and then all namespaces from the specified * namespace down. */ void GetValues(EConfigNamespace ns, const CStr& name, CConfigValueSet& values) const; /** * Returns the namespace that the value returned by GetValues was defined in, * or CFG_LAST if it wasn't defined at all. */ EConfigNamespace GetValueNamespace(EConfigNamespace ns, const CStr& name) const; /** * Retrieve a map of values corresponding to settings whose names begin * with the given prefix; * will search all namespaces from default up to the specified namespace. */ std::map GetValuesWithPrefix(EConfigNamespace ns, const CStr& prefix) const; /** * Save a config value in the specified namespace. If the config variable * existed the value is replaced. */ void SetValueString(EConfigNamespace ns, const CStr& name, const CStr& value); void SetValueBool(EConfigNamespace ns, const CStr& name, const bool value); void SetValueList(EConfigNamespace ns, const CStr& name, std::vector values); /** * Remove a config value in the specified namespace. */ void RemoveValue(EConfigNamespace ns, const CStr& name); /** * Set the path to the config file used to populate the specified namespace * Note that this function does not actually load the config file. Use * the Reload() method if you want to read the config file at the same time. * * 'path': The path to the config file. */ void SetConfigFile(EConfigNamespace ns, const VfsPath& path); /** * Reload the config file associated with the specified config namespace * (the last config file path set with SetConfigFile) * * Returns: * true: if the reload succeeded, * false: if the reload failed */ bool Reload(EConfigNamespace); /** * Write the current state of the specified config namespace to the file * specified by 'path' * * Returns: * true: if the config namespace was successfully written to the file * false: if an error occurred */ bool WriteFile(EConfigNamespace ns, const VfsPath& path) const; /** * Write the current state of the specified config namespace to the file * it was originally loaded from. * * Returns: * true: if the config namespace was successfully written to the file * false: if an error occurred */ bool WriteFile(EConfigNamespace ns) const; /** * Write a config value to the file specified by 'path' * * Returns: * true: if the config value was successfully saved and written to the file * false: if an error occurred */ bool WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value, const VfsPath& path); bool WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value); + + // Opaque data type so that callers that hook into ConfigDB can delete their hooks. + class hook_t + { + friend class CConfigDB; + public: + // Point the moved-from hook to end, which is checked for in UnregisterHook, + // to avoid a double-erase error. + hook_t(hook_t&& h) { ptr = std::move(h.ptr); h.ptr = m_Hooks.end(); } + hook_t(const hook_t&) = delete; + private: + hook_t(std::multimap>::iterator p) : ptr(p) {}; + + std::multimap>::iterator ptr; + }; + + /** + * Register a simple lambda that will be called anytime the value changes in any namespace + * This is simple on purpose, the hook is responsible for checking if it should do something. + * When RegisterHookAndCall is called, the hook is immediately triggered. + */ + hook_t RegisterHookAndCall(const CStr& name, std::function hook); + + void UnregisterHook(hook_t&& hook); + private: static std::map m_Map[]; + static std::multimap> m_Hooks; static VfsPath m_ConfigFile[]; static bool m_HasChanges[]; }; // stores the value of the given key into . this quasi-template // convenience wrapper on top of GetValue simplifies user code #define CFG_GET_VAL(name, destination)\ g_ConfigDB.GetValue(CFG_USER, name, destination) #endif // INCLUDED_CONFIGDB Index: ps/trunk/source/renderer/Renderer.h =================================================================== --- ps/trunk/source/renderer/Renderer.h (revision 24227) +++ ps/trunk/source/renderer/Renderer.h (revision 24228) @@ -1,460 +1,460 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * higher level interface on top of OpenGL to render basic objects: * terrain, models, sprites, particles etc. */ #ifndef INCLUDED_RENDERER #define INCLUDED_RENDERER #include "graphics/Camera.h" #include "graphics/SColor.h" #include "graphics/ShaderProgramPtr.h" #include "lib/file/vfs/vfs_path.h" #include "lib/res/handle.h" #include "ps/Singleton.h" #include "graphics/ShaderDefines.h" #include "renderer/Scene.h" #include "renderer/RenderingOptions.h" // necessary declarations class CFontManager; class CLightEnv; class CMaterial; class CMaterialManager; class CModel; class CParticleManager; class CPatch; class CPostprocManager; class CShaderManager; class CSimulation2; class CTextureManager; class CTimeManager; class RenderPathVertexShader; class ShadowMap; class SkyManager; class TerrainRenderer; class WaterManager; // rendering modes enum ERenderMode { WIREFRAME, SOLID, EDGED_FACES }; // transparency modes enum ETransparentMode { TRANSPARENT, TRANSPARENT_OPAQUE, TRANSPARENT_BLEND }; // access to sole renderer object #define g_Renderer CRenderer::GetSingleton() /////////////////////////////////////////////////////////////////////////////////////////// // CRenderer: base renderer class - primary interface to the rendering engine struct CRendererInternals; class CRenderer : public Singleton, private SceneCollector { public: // various enumerations and renderer related constants enum { NumAlphaMaps=14 }; enum CullGroup { CULL_DEFAULT, CULL_SHADOWS, CULL_REFLECTIONS, CULL_REFRACTIONS, CULL_SILHOUETTE_OCCLUDER, CULL_SILHOUETTE_CASTER, CULL_MAX }; // stats class - per frame counts of number of draw calls, poly counts etc struct Stats { // set all stats to zero void Reset() { memset(this, 0, sizeof(*this)); } // number of draw calls per frame - total DrawElements + Begin/End immediate mode loops size_t m_DrawCalls; // number of terrain triangles drawn size_t m_TerrainTris; // number of water triangles drawn size_t m_WaterTris; // number of (non-transparent) model triangles drawn size_t m_ModelTris; // number of overlay triangles drawn size_t m_OverlayTris; // number of splat passes for alphamapping size_t m_BlendSplats; // number of particles size_t m_Particles; }; struct Caps { bool m_VBO; bool m_ARBProgram; bool m_ARBProgramShadow; bool m_VertexShader; bool m_FragmentShader; bool m_Shadows; bool m_PrettyWater; }; public: // constructor, destructor CRenderer(); ~CRenderer(); // open up the renderer: performs any necessary initialisation bool Open(int width,int height); // resize renderer view void Resize(int width,int height); // return view width int GetWidth() const { return m_Width; } // return view height int GetHeight() const { return m_Height; } // return view aspect ratio float GetAspect() const { return float(m_Width)/float(m_Height); } // signal frame start void BeginFrame(); // signal frame end void EndFrame(); /** * Should be called after each SwapBuffers call. */ void OnSwapBuffers(); /** * Set simulation context for rendering purposes. * Must be called at least once when the game has started and before * frames are rendered. */ void SetSimulation(CSimulation2* simulation); // set color used to clear screen in BeginFrame() void SetClearColor(SColor4ub color); // trigger a reload of shaders (when parameters they depend on have changed) void MakeShadersDirty(); /** * Set up the camera used for rendering the next scene; this includes * setting OpenGL state like viewport, projection and modelview matrices. * * @param viewCamera this camera determines the eye position for rendering * @param cullCamera this camera determines the frustum for culling in the renderer and * for shadow calculations */ void SetSceneCamera(const CCamera& viewCamera, const CCamera& cullCamera); // set the viewport void SetViewport(const SViewPort &); // get the last viewport SViewPort GetViewport(); /** * Render the given scene immediately. * @param scene a Scene object describing what should be rendered. */ void RenderScene(Scene& scene); /** * Return the scene that is currently being rendered. * Only valid when the renderer is in a RenderScene call. */ Scene& GetScene(); /** * Render text overlays on top of the scene. * Assumes the caller has set up the GL environment for orthographic rendering * with texturing and blending. */ void RenderTextOverlays(); // set the current lighting environment; (note: the passed pointer is just copied to a variable within the renderer, // so the lightenv passed must be scoped such that it is not destructed until after the renderer is no longer rendering) void SetLightEnv(CLightEnv* lightenv) { m_LightEnv=lightenv; } // set the mode to render subsequent terrain patches void SetTerrainRenderMode(ERenderMode mode) { m_TerrainRenderMode = mode; } // get the mode to render subsequent terrain patches ERenderMode GetTerrainRenderMode() const { return m_TerrainRenderMode; } // set the mode to render subsequent water patches void SetWaterRenderMode(ERenderMode mode) { m_WaterRenderMode = mode; } // get the mode to render subsequent water patches ERenderMode GetWaterRenderMode() const { return m_WaterRenderMode; } // set the mode to render subsequent models void SetModelRenderMode(ERenderMode mode) { m_ModelRenderMode = mode; } // get the mode to render subsequent models ERenderMode GetModelRenderMode() const { return m_ModelRenderMode; } // Get the mode to render subsequent overlays. ERenderMode GetOverlayRenderMode() const { return m_OverlayRenderMode; } // Set the mode to render subsequent overlays. void SetOverlayRenderMode(ERenderMode mode) { m_OverlayRenderMode = mode; } // debugging void SetDisplayTerrainPriorities(bool enabled) { m_DisplayTerrainPriorities = enabled; } // bind a GL texture object to active unit void BindTexture(int unit, unsigned int tex); // load the default set of alphamaps. // return a negative error code if anything along the way fails. // called via delay-load mechanism. int LoadAlphaMaps(); void UnloadAlphaMaps(); // return stats accumulated for current frame Stats& GetStats() { return m_Stats; } // return the current light environment const CLightEnv &GetLightEnv() { return *m_LightEnv; } // return the current view camera const CCamera& GetViewCamera() const { return m_ViewCamera; } // replace the current view camera void SetViewCamera(const CCamera& camera) { m_ViewCamera = camera; } // return the current cull camera const CCamera& GetCullCamera() const { return m_CullCamera; } /** * GetWaterManager: Return the renderer's water manager. * * @return the WaterManager object used by the renderer */ WaterManager* GetWaterManager() { return m_WaterManager; } /** * GetSkyManager: Return the renderer's sky manager. * * @return the SkyManager object used by the renderer */ SkyManager* GetSkyManager() { return m_SkyManager; } CTextureManager& GetTextureManager(); CShaderManager& GetShaderManager(); CParticleManager& GetParticleManager(); TerrainRenderer& GetTerrainRenderer(); CMaterialManager& GetMaterialManager(); CFontManager& GetFontManager(); CShaderDefines GetSystemShaderDefines() { return m_SystemShaderDefines; } CTimeManager& GetTimeManager(); CPostprocManager& GetPostprocManager(); /** * GetCapabilities: Return which OpenGL capabilities are available and enabled. * * @return capabilities structure */ const Caps& GetCapabilities() const { return m_Caps; } ShadowMap& GetShadowMap(); /** * Resets the render state to default, that was before a game started */ void ResetState(); protected: friend struct CRendererInternals; friend class CVertexBuffer; friend class CPatchRData; friend class CDecalRData; friend class FixedFunctionModelRenderer; friend class ModelRenderer; friend class PolygonSortModelRenderer; friend class SortModelRenderer; friend class RenderPathVertexShader; friend class HWLightingModelRenderer; friend class ShaderModelVertexRenderer; friend class InstancingModelRenderer; friend class ShaderInstancingModelRenderer; friend class TerrainRenderer; friend class WaterRenderer; - friend struct SRenderingOptions; + friend class CRenderingOptions; //BEGIN: Implementation of SceneCollector void Submit(CPatch* patch); void Submit(SOverlayLine* overlay); void Submit(SOverlayTexturedLine* overlay); void Submit(SOverlaySprite* overlay); void Submit(SOverlayQuad* overlay); void Submit(CModelDecal* decal); void Submit(CParticleEmitter* emitter); void Submit(SOverlaySphere* overlay); void SubmitNonRecursive(CModel* model); //END: Implementation of SceneCollector // render any batched objects void RenderSubmissions(const CBoundingBoxAligned& waterScissor); // patch rendering stuff void RenderPatches(const CShaderDefines& context, int cullGroup); // model rendering stuff void RenderModels(const CShaderDefines& context, int cullGroup); void RenderTransparentModels(const CShaderDefines& context, int cullGroup, ETransparentMode transparentMode, bool disableFaceCulling); void RenderSilhouettes(const CShaderDefines& context); void RenderParticles(int cullGroup); // shadow rendering stuff void RenderShadowMap(const CShaderDefines& context); // render water reflection and refraction textures void RenderReflections(const CShaderDefines& context, const CBoundingBoxAligned& scissor); void RenderRefractions(const CShaderDefines& context, const CBoundingBoxAligned& scissor); void ComputeReflectionCamera(CCamera& camera, const CBoundingBoxAligned& scissor) const; void ComputeRefractionCamera(CCamera& camera, const CBoundingBoxAligned& scissor) const; // debugging void DisplayFrustum(); // enable oblique frustum clipping with the given clip plane void SetObliqueFrustumClipping(CCamera& camera, const CVector4D& clipPlane) const; void SetRenderPath(RenderPath rp); void ReloadShaders(); void RecomputeSystemShaderDefines(); // hotloading static Status ReloadChangedFileCB(void* param, const VfsPath& path); // RENDERER DATA: /// Private data that is not needed by inline functions CRendererInternals* m; // view width int m_Width; // view height int m_Height; // Current terrain rendering mode. ERenderMode m_TerrainRenderMode; // Current water rendering mode. ERenderMode m_WaterRenderMode; // Current model rendering mode. ERenderMode m_ModelRenderMode; // Current overlay rendering mode. ERenderMode m_OverlayRenderMode; CShaderDefines m_SystemShaderDefines; SViewPort m_Viewport; /** * m_ViewCamera: determines the eye position for rendering * * @see CGameView::m_ViewCamera */ CCamera m_ViewCamera; /** * m_CullCamera: determines the frustum for culling and shadowmap calculations * * @see CGameView::m_ViewCamera */ CCamera m_CullCamera; // only valid inside a call to RenderScene Scene* m_CurrentScene; int m_CurrentCullGroup; // color used to clear screen in BeginFrame float m_ClearColor[4]; // current lighting setup CLightEnv* m_LightEnv; // ogl_tex handle of composite alpha map (all the alpha maps packed into one texture) Handle m_hCompositeAlphaMap; // coordinates of each (untransformed) alpha map within the packed texture struct { float u0,u1,v0,v1; } m_AlphaMapCoords[NumAlphaMaps]; // card capabilities Caps m_Caps; // build card cap bits void EnumCaps(); // per-frame renderer stats Stats m_Stats; /** * m_WaterManager: the WaterManager object used for water textures and settings * (e.g. water color, water height) */ WaterManager* m_WaterManager; /** * m_SkyManager: the SkyManager object used for sky textures and settings */ SkyManager* m_SkyManager; /** * Enable rendering of terrain tile priority text overlay, for debugging. */ bool m_DisplayTerrainPriorities; public: /** * m_ShadowZBias: Z bias used when rendering shadows into a depth texture. * This can be used to control shadowing artifacts. * * Can be accessed via JS as renderer.shadowZBias * ShadowMap uses this for matrix calculation. */ float m_ShadowZBias; /** * m_ShadowMapSize: Size of shadow map, or 0 for default. Typically slow but useful * for high-quality rendering. Changes don't take effect until the shadow map * is regenerated. * * Can be accessed via JS as renderer.shadowMapSize */ int m_ShadowMapSize; /** * m_SkipSubmit: Disable the actual submission of rendering commands to OpenGL. * All state setup is still performed as usual. * * Can be accessed via JS as renderer.skipSubmit */ bool m_SkipSubmit; }; #endif Index: ps/trunk/source/renderer/RenderingOptions.cpp =================================================================== --- ps/trunk/source/renderer/RenderingOptions.cpp (revision 24227) +++ ps/trunk/source/renderer/RenderingOptions.cpp (revision 24228) @@ -1,161 +1,230 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "RenderingOptions.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStr.h" #include "renderer/Renderer.h" +#include "renderer/PostprocManager.h" +#include "renderer/ShadowMap.h" -SRenderingOptions g_RenderingOptions; +CRenderingOptions g_RenderingOptions; + +class CRenderingOptions::ConfigHooks +{ +public: + std::vector::iterator begin() { return hooks.begin(); } + std::vector::iterator end() { return hooks.end(); } + void insert(CConfigDB::hook_t&& hook) { return hooks.emplace_back(std::move(hook)); } +private: + std::vector hooks; +}; RenderPath RenderPathEnum::FromString(const CStr8& name) { if (name == "default") return DEFAULT; if (name == "fixed") return FIXED; if (name == "shader") return SHADER; LOGWARNING("Unknown render path %s", name.c_str()); return DEFAULT; } CStr8 RenderPathEnum::ToString(RenderPath path) { switch (path) { case RenderPath::DEFAULT: return "default"; case RenderPath::FIXED: return "fixed"; case RenderPath::SHADER: return "shader"; } return "default"; // Silence warning about reaching end of non-void function. } -SRenderingOptions::SRenderingOptions() +CRenderingOptions::CRenderingOptions() : m_ConfigHooks(new ConfigHooks()) { m_NoVBO = false; m_RenderPath = RenderPath::DEFAULT; m_Shadows = false; m_WaterEffects = false; m_WaterFancyEffects = false; m_WaterRealDepth = false; m_WaterRefraction = false; m_WaterReflection = false; m_WaterShadows = false; m_ShadowAlphaFix = true; m_ARBProgramShadow = true; m_ShadowPCF = false; m_Particles = false; m_Silhouettes = false; m_PreferGLSL = false; m_Fog = false; m_ForceAlphaTest = false; m_GPUSkinning = false; m_SmoothLOS = false; m_PostProc = false; m_ShowSky = false; m_DisplayFrustum = false; m_DisplayShadowsFrustum = false; m_RenderActors = true; } -void SRenderingOptions::ReadConfig() +CRenderingOptions::~CRenderingOptions() { - CFG_GET_VAL("shadows", m_Shadows); - CFG_GET_VAL("shadowpcf", m_ShadowPCF); - - CFG_GET_VAL("preferglsl", m_PreferGLSL); - CFG_GET_VAL("postproc", m_PostProc); - CFG_GET_VAL("smoothlos", m_SmoothLOS); - - CStr renderPath; - CFG_GET_VAL("renderpath", renderPath); - SetRenderPath(RenderPathEnum::FromString(renderPath)); - - CFG_GET_VAL("watereffects", m_WaterEffects); - CFG_GET_VAL("waterfancyeffects", m_WaterFancyEffects); - CFG_GET_VAL("waterrealdepth", m_WaterRealDepth); - CFG_GET_VAL("waterrefraction", m_WaterRefraction); - CFG_GET_VAL("waterreflection", m_WaterReflection); - CFG_GET_VAL("watershadows", m_WaterShadows); - - CFG_GET_VAL("particles", m_Particles); - CFG_GET_VAL("fog", m_Fog); - CFG_GET_VAL("silhouettes", m_Silhouettes); - CFG_GET_VAL("showsky", m_ShowSky); - - CFG_GET_VAL("novbo", m_NoVBO); + // This is currently irrelevant since CConfigDB is deleted before CRenderingOptions + // (as only the latter is a static variable), but the check is a good idea regardless. + if (!CConfigDB::IsInitialised()) + return; + for (CConfigDB::hook_t& hook : *m_ConfigHooks) + g_ConfigDB.UnregisterHook(std::move(hook)); +} + +template +void CRenderingOptions::SetupConfig(CStr8 name, T& variable) +{ + m_ConfigHooks->insert(g_ConfigDB.RegisterHookAndCall(name, [name, &variable]() { CFG_GET_VAL(name, variable); })); +} + +void CRenderingOptions::SetupConfig(CStr8 name, std::function hook) +{ + m_ConfigHooks->insert(g_ConfigDB.RegisterHookAndCall(name, hook)); +} + +void CRenderingOptions::ReadConfig() +{ + SetupConfig("preferglsl", [this]() { + bool enabled; + CFG_GET_VAL("preferglsl", enabled); + SetPreferGLSL(enabled); + }); + + SetupConfig("shadowquality", []() { + g_Renderer.GetShadowMap().RecreateTexture(); + }); + + SetupConfig("shadows", [this]() { + bool enabled; + CFG_GET_VAL("shadows", enabled); + SetShadows(enabled); + }); + SetupConfig("shadowpcf", [this]() { + bool enabled; + CFG_GET_VAL("shadowpcf", enabled); + SetShadowPCF(enabled); + }); + + SetupConfig("antialiasing", []() { + g_Renderer.GetPostprocManager().UpdateAntiAliasingTechnique(); + }); + + SetupConfig("sharpness", []() { + g_Renderer.GetPostprocManager().UpdateSharpnessFactor(); + }); + + SetupConfig("sharpening", []() { + g_Renderer.GetPostprocManager().UpdateSharpeningTechnique(); + }); + + SetupConfig("postproc", m_PostProc); + SetupConfig("smoothlos", m_SmoothLOS); + + SetupConfig("renderpath", [this]() { + CStr renderPath; + CFG_GET_VAL("renderpath", renderPath); + SetRenderPath(RenderPathEnum::FromString(renderPath)); + }); + + SetupConfig("watereffects", m_WaterEffects); + SetupConfig("waterfancyeffects", m_WaterFancyEffects); + SetupConfig("waterrealdepth", m_WaterRealDepth); + SetupConfig("waterrefraction", m_WaterRefraction); + SetupConfig("waterreflection", m_WaterReflection); + SetupConfig("watershadows", m_WaterShadows); + + SetupConfig("particles", m_Particles); + SetupConfig("fog", [this]() { + bool enabled; + CFG_GET_VAL("fog", enabled); + SetFog(enabled); + }); + SetupConfig("silhouettes", m_Silhouettes); + SetupConfig("showsky", m_ShowSky); + + SetupConfig("novbo", m_NoVBO); + + SetupConfig("forcealphatest", m_ForceAlphaTest); + SetupConfig("gpuskinning", [this]() { + bool enabled; + CFG_GET_VAL("gpuskinning", enabled); + if (enabled && !m_PreferGLSL) + LOGWARNING("GPUSkinning has been disabled, because it is not supported with PreferGLSL disabled."); + else if (enabled) + m_GPUSkinning = true; + }); - CFG_GET_VAL("forcealphatest", m_ForceAlphaTest); - CFG_GET_VAL("gpuskinning", m_GPUSkinning); - - CFG_GET_VAL("renderactors", m_RenderActors); - - if (m_GPUSkinning && !m_PreferGLSL) - { - LOGWARNING("GPUSkinning have been disabled, because it is not supported with PreferGLSL disabled."); - m_GPUSkinning = false; - } + SetupConfig("renderactors", m_RenderActors); } -void SRenderingOptions::SetShadows(bool value) +void CRenderingOptions::SetShadows(bool value) { m_Shadows = value; g_Renderer.MakeShadersDirty(); } -void SRenderingOptions::SetShadowPCF(bool value) +void CRenderingOptions::SetShadowPCF(bool value) { m_ShadowPCF = value; g_Renderer.MakeShadersDirty(); } -void SRenderingOptions::SetFog(bool value) +void CRenderingOptions::SetFog(bool value) { m_Fog = value; g_Renderer.MakeShadersDirty(); } -void SRenderingOptions::SetPreferGLSL(bool value) +void CRenderingOptions::SetPreferGLSL(bool value) { if (m_GPUSkinning && !value) { LOGWARNING("GPUSkinning have been disabled, because it is not supported with PreferGLSL disabled."); m_GPUSkinning = false; } else if (!m_GPUSkinning && value) CFG_GET_VAL("gpuskinning", m_GPUSkinning); m_PreferGLSL = value; g_Renderer.MakeShadersDirty(); g_Renderer.RecomputeSystemShaderDefines(); } -void SRenderingOptions::SetRenderPath(RenderPath value) +void CRenderingOptions::SetRenderPath(RenderPath value) { m_RenderPath = value; g_Renderer.SetRenderPath(m_RenderPath); } Index: ps/trunk/source/renderer/RenderingOptions.h =================================================================== --- ps/trunk/source/renderer/RenderingOptions.h (revision 24227) +++ ps/trunk/source/renderer/RenderingOptions.h (revision 24228) @@ -1,115 +1,134 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /** * Keeps track of the settings used for rendering. * Ideally this header file should remain very quick to parse, * so avoid including other headers here unless absolutely necessary. */ #ifndef INCLUDED_RENDERINGOPTIONS #define INCLUDED_RENDERINGOPTIONS class CStr8; class CRenderer; enum RenderPath { // If no rendering path is configured explicitly, the renderer // will choose the path when Open() is called. DEFAULT, // Classic fixed function. FIXED, // Use new ARB/GLSL system SHADER }; struct RenderPathEnum { static RenderPath FromString(const CStr8& name); static CStr8 ToString(RenderPath); }; -struct SRenderingOptions +class CRenderingOptions { // The renderer needs access to our private variables directly because capabilities have not yet been extracted // and thus sometimes it needs to change the rendering options without the side-effects. friend class CRenderer; - SRenderingOptions(); +public: + CRenderingOptions(); + ~CRenderingOptions(); + void ReadConfig(); #define OPTION_DEFAULT_SETTER(NAME, TYPE) \ public: void Set##NAME(TYPE value) { m_##NAME = value; }\ #define OPTION_CUSTOM_SETTER(NAME, TYPE) \ public: void Set##NAME(TYPE value);\ #define OPTION_GETTER(NAME, TYPE)\ public: TYPE Get##NAME() const { return m_##NAME; }\ #define OPTION_DEF(NAME, TYPE)\ private: TYPE m_##NAME; #define OPTION(NAME, TYPE)\ OPTION_DEFAULT_SETTER(NAME, TYPE); OPTION_GETTER(NAME, TYPE); OPTION_DEF(NAME, TYPE); #define OPTION_WITH_SIDE_EFFECT(NAME, TYPE)\ OPTION_CUSTOM_SETTER(NAME, TYPE); OPTION_GETTER(NAME, TYPE); OPTION_DEF(NAME, TYPE); OPTION(NoVBO, bool); OPTION_WITH_SIDE_EFFECT(Shadows, bool); OPTION_WITH_SIDE_EFFECT(ShadowPCF, bool); OPTION_WITH_SIDE_EFFECT(PreferGLSL, bool); OPTION_WITH_SIDE_EFFECT(Fog, bool); OPTION_WITH_SIDE_EFFECT(RenderPath, RenderPath); OPTION(WaterEffects, bool); OPTION(WaterFancyEffects, bool); OPTION(WaterRealDepth, bool); OPTION(WaterRefraction, bool); OPTION(WaterReflection, bool); OPTION(WaterShadows, bool); OPTION(ShadowAlphaFix, bool); OPTION(ARBProgramShadow, bool); OPTION(Particles, bool); OPTION(ForceAlphaTest, bool); OPTION(GPUSkinning, bool); OPTION(Silhouettes, bool); OPTION(SmoothLOS, bool); OPTION(ShowSky, bool); OPTION(PostProc, bool); OPTION(DisplayFrustum, bool); OPTION(DisplayShadowsFrustum, bool); OPTION(RenderActors, bool); #undef OPTION_DEFAULT_SETTER #undef OPTION_CUSTOM_SETTER #undef OPTION_GETTER #undef OPTION_DEF #undef OPTION #undef OPTION_WITH_SIDE_EFFECT + +private: + /** + * Registers a config hook for config variable @name that updates @variable. + * Also immediately updates variable with the value of the config. + */ + template + void SetupConfig(CStr8 name, T& variable); + /** + * Registers a config hook for config variable @name. + * Also immediately triggers the hook. + */ + void SetupConfig(CStr8 name, std::function hook); + + class ConfigHooks; + std::unique_ptr m_ConfigHooks; // Hide this via PImpl to avoid including ConfigDB.h here. }; -extern SRenderingOptions g_RenderingOptions; +extern CRenderingOptions g_RenderingOptions; #endif // INCLUDED_RENDERINGOPTIONS Index: ps/trunk/source/renderer/scripting/JSInterface_Renderer.cpp =================================================================== --- ps/trunk/source/renderer/scripting/JSInterface_Renderer.cpp (revision 24227) +++ ps/trunk/source/renderer/scripting/JSInterface_Renderer.cpp (revision 24228) @@ -1,127 +1,65 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_Renderer.h" #include "graphics/TextureManager.h" -#include "renderer/PostprocManager.h" #include "renderer/RenderingOptions.h" #include "renderer/Renderer.h" -#include "renderer/ShadowMap.h" #include "scriptinterface/ScriptInterface.h" #define IMPLEMENT_BOOLEAN_SCRIPT_SETTING(NAME) \ -bool JSI_Renderer::Get##NAME##Enabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) \ +bool Get##NAME##Enabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) \ { \ return g_RenderingOptions.Get##NAME(); \ } \ \ -void JSI_Renderer::Set##NAME##Enabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool enabled) \ +void Set##NAME##Enabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool enabled) \ { \ g_RenderingOptions.Set##NAME(enabled); \ } -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(Shadows); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(ShadowPCF); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(Particles); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(PreferGLSL); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(WaterEffects); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(WaterFancyEffects); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(WaterRealDepth); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(WaterReflection); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(WaterRefraction); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(WaterShadows); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(Fog); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(Silhouettes); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(ShowSky); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(SmoothLOS); -IMPLEMENT_BOOLEAN_SCRIPT_SETTING(PostProc); IMPLEMENT_BOOLEAN_SCRIPT_SETTING(DisplayFrustum); IMPLEMENT_BOOLEAN_SCRIPT_SETTING(DisplayShadowsFrustum); #undef IMPLEMENT_BOOLEAN_SCRIPT_SETTING std::string JSI_Renderer::GetRenderPath(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { return RenderPathEnum::ToString(g_RenderingOptions.GetRenderPath()); } -void JSI_Renderer::SetRenderPath(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& name) -{ - g_RenderingOptions.SetRenderPath(RenderPathEnum::FromString(name)); -} - -void JSI_Renderer::UpdateAntiAliasingTechnique(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - g_Renderer.GetPostprocManager().UpdateAntiAliasingTechnique(); -} - -void JSI_Renderer::UpdateSharpeningTechnique(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - g_Renderer.GetPostprocManager().UpdateSharpeningTechnique(); -} - -void JSI_Renderer::UpdateSharpnessFactor(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - g_Renderer.GetPostprocManager().UpdateSharpnessFactor(); -} - -void JSI_Renderer::RecreateShadowMap(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - g_Renderer.GetShadowMap().RecreateTexture(); -} - bool JSI_Renderer::TextureExists(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename) { return g_Renderer.GetTextureManager().TextureExists(filename); } #define REGISTER_BOOLEAN_SCRIPT_SETTING(NAME) \ -scriptInterface.RegisterFunction("Renderer_Get" #NAME "Enabled"); \ -scriptInterface.RegisterFunction("Renderer_Set" #NAME "Enabled"); +scriptInterface.RegisterFunction("Renderer_Get" #NAME "Enabled"); \ +scriptInterface.RegisterFunction("Renderer_Set" #NAME "Enabled"); void JSI_Renderer::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("Renderer_GetRenderPath"); - scriptInterface.RegisterFunction("Renderer_SetRenderPath"); - scriptInterface.RegisterFunction("Renderer_RecreateShadowMap"); - scriptInterface.RegisterFunction("Renderer_UpdateAntiAliasingTechnique"); - scriptInterface.RegisterFunction("Renderer_UpdateSharpeningTechnique"); - scriptInterface.RegisterFunction("Renderer_UpdateSharpnessFactor"); scriptInterface.RegisterFunction("TextureExists"); - REGISTER_BOOLEAN_SCRIPT_SETTING(Shadows); - REGISTER_BOOLEAN_SCRIPT_SETTING(ShadowPCF); - REGISTER_BOOLEAN_SCRIPT_SETTING(Particles); - REGISTER_BOOLEAN_SCRIPT_SETTING(PreferGLSL); - REGISTER_BOOLEAN_SCRIPT_SETTING(WaterEffects); - REGISTER_BOOLEAN_SCRIPT_SETTING(WaterFancyEffects); - REGISTER_BOOLEAN_SCRIPT_SETTING(WaterRealDepth); - REGISTER_BOOLEAN_SCRIPT_SETTING(WaterReflection); - REGISTER_BOOLEAN_SCRIPT_SETTING(WaterRefraction); - REGISTER_BOOLEAN_SCRIPT_SETTING(WaterShadows); - REGISTER_BOOLEAN_SCRIPT_SETTING(Fog); - REGISTER_BOOLEAN_SCRIPT_SETTING(Silhouettes); - REGISTER_BOOLEAN_SCRIPT_SETTING(ShowSky); - REGISTER_BOOLEAN_SCRIPT_SETTING(SmoothLOS); - REGISTER_BOOLEAN_SCRIPT_SETTING(PostProc); REGISTER_BOOLEAN_SCRIPT_SETTING(DisplayFrustum); REGISTER_BOOLEAN_SCRIPT_SETTING(DisplayShadowsFrustum); } #undef REGISTER_BOOLEAN_SCRIPT_SETTING Index: ps/trunk/source/renderer/scripting/JSInterface_Renderer.h =================================================================== --- ps/trunk/source/renderer/scripting/JSInterface_Renderer.h (revision 24227) +++ ps/trunk/source/renderer/scripting/JSInterface_Renderer.h (revision 24228) @@ -1,60 +1,33 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_JSINTERFACE_RENDERER #define INCLUDED_JSINTERFACE_RENDERER #include "scriptinterface/ScriptInterface.h" -#define DECLARE_BOOLEAN_SCRIPT_SETTING(NAME) \ - bool Get##NAME##Enabled(ScriptInterface::CmptPrivate* pCmptPrivate); \ - void Set##NAME##Enabled(ScriptInterface::CmptPrivate* pCmptPrivate, bool Enabled); - namespace JSI_Renderer { std::string GetRenderPath(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetRenderPath(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name); - void UpdateAntiAliasingTechnique(ScriptInterface::CmptPrivate* pCmptPrivate); - void UpdateSharpeningTechnique(ScriptInterface::CmptPrivate* pCmptPrivate); - void UpdateSharpnessFactor(ScriptInterface::CmptPrivate* pCmptPrivate); - void RecreateShadowMap(ScriptInterface::CmptPrivate* pCmptPrivate); bool TextureExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename); - DECLARE_BOOLEAN_SCRIPT_SETTING(Shadows); - DECLARE_BOOLEAN_SCRIPT_SETTING(ShadowPCF); - DECLARE_BOOLEAN_SCRIPT_SETTING(Particles); - DECLARE_BOOLEAN_SCRIPT_SETTING(PreferGLSL); - DECLARE_BOOLEAN_SCRIPT_SETTING(WaterEffects); - DECLARE_BOOLEAN_SCRIPT_SETTING(WaterFancyEffects); - DECLARE_BOOLEAN_SCRIPT_SETTING(WaterRealDepth); - DECLARE_BOOLEAN_SCRIPT_SETTING(WaterReflection); - DECLARE_BOOLEAN_SCRIPT_SETTING(WaterRefraction); - DECLARE_BOOLEAN_SCRIPT_SETTING(WaterShadows); - DECLARE_BOOLEAN_SCRIPT_SETTING(Fog); - DECLARE_BOOLEAN_SCRIPT_SETTING(Silhouettes); - DECLARE_BOOLEAN_SCRIPT_SETTING(ShowSky); - DECLARE_BOOLEAN_SCRIPT_SETTING(SmoothLOS); - DECLARE_BOOLEAN_SCRIPT_SETTING(PostProc); - DECLARE_BOOLEAN_SCRIPT_SETTING(DisplayFrustum); - DECLARE_BOOLEAN_SCRIPT_SETTING(DisplayShadowsFrustum); - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); } #undef DECLARE_BOOLEAN_SCRIPT_SETTING #endif // INCLUDED_JSINTERFACE_RENDERER