Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 24617) +++ ps/trunk/binaries/data/config/default.cfg (revision 24618) @@ -1,538 +1,538 @@ ; Global Configuration Settings ; ; ************************************************************** ; * DO NOT EDIT THIS FILE if you want personal customisations: * ; * create a text file called "local.cfg" instead, and copy * ; * the lines from this file that you want to change. * ; * * ; * If a setting is part of a section (for instance [hotkey]) * ; * you need to append the section name at the beginning of * ; * your custom line (for instance you need to write * ; * "hotkey.pause = Space" if you want to change the pausing * ; * hotkey to the spacebar). * ; * * ; * On Linux, create: * ; * $XDG_CONFIG_HOME/0ad/config/local.cfg * ; * (Note: $XDG_CONFIG_HOME defaults to ~/.config) * ; * * ; * On OS X, create: * ; * ~/Library/Application\ Support/0ad/config/local.cfg * ; * * ; * On Windows, create: * ; * %appdata%\0ad\config\local.cfg * ; * * ; ************************************************************** ; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.) windowed = false ; Show detailed tooltips (Unit stats) showdetailedtooltips = false ; Pause the game on window focus loss (Only applicable to single player mode) pauseonfocusloss = true ; Persist settings after leaving the game setup screen persistmatchsettings = true ; Default player name to use in multiplayer ; playername = "anonymous" ; Default server name or IP to use in multiplayer multiplayerserver = "127.0.0.1" ; Force a particular resolution. (If these are 0, the default is ; to keep the current desktop resolution in fullscreen mode or to ; use 1024x768 in windowed mode.) xres = 0 yres = 0 ; Force a non-standard bit depth (if 0 then use the current desktop bit depth) bpp = 0 ; Preferred display (for multidisplay setups, only works with SDL 2.0) display = 0 ; Emulate right-click with Ctrl+Click on Mac mice macmouse = false ; System settings: ; if false, actors won't be rendered but anything entity will be. renderactors = true watereffects=true ; When disabled, force usage of the fixed pipeline water. This is faster, but really, really ugly. waterfancyeffects = false waterrealdepth = true waterrefraction = true waterreflection = true watershadows = false shadows = true shadowquality = 0 ; Shadow map resolution. (-2 - Very Low, -1 - Low, 0 - Medium, 1 - High, 2 - Very High) ; High values can crash the game when using a graphics card with low memory! shadowpcf = true shadowsfixed = false ; When enabled shadows are rendered only on the shadowsfixeddistance = 300.0 ; fixed distance and without swimming effect. vsync = false particles = true fog = true silhouettes = true showsky = true novbo = false ; Disable hardware cursors nohwcursor = false ; Specify the render path. This can be one of: ; default Automatically select one of the below, depending on system capabilities ; fixed Only use OpenGL fixed function pipeline ; shader Use vertex/fragment shaders for transform and lighting where possible ; Using 'fixed' instead of 'default' may work around some graphics-related problems, ; but will reduce performance and features when a modern graphics card is available. renderpath = default ;;;;; EXPERIMENTAL ;;;;; ; Prefer GLSL shaders over ARB shaders. Allows fancier graphical effects. preferglsl = false ; Experimental probably-non-working GPU skinning support; requires preferglsl; use at own risk gpuskinning = false ; Use smooth LOS interpolation smoothlos = false ; Use screen-space postprocessing filters (HDR, bloom, DOF, etc). Incompatible with fixed renderpath. postproc = false ; Use anti-aliasing techniques. antialiasing = "disabled" ; Use sharpening techniques. sharpening = "disabled" sharpness = 0.3 ; Quality level of shader effects (set to 10 to display all effects) materialmgr.quality = 2.0 ; Maximum distance to display parallax effect. Set to 0 to disable parallax. materialmgr.PARALLAX_DIST.max = 150 ; Maximum distance to display high quality parallax effect. materialmgr.PARALLAX_HQ_DIST.max = 75 ; Maximum distance to display very high quality parallax effect. Set to 30 to enable. materialmgr.PARALLAX_VHQ_DIST.max = 0 ;;;;;;;;;;;;;;;;;;;;;;;; ; Replace alpha-blending with alpha-testing, for performance experiments forcealphatest = false ; Color of the sky (in "r g b" format) skycolor = "0 0 0" [adaptivefps] session = 60 ; Throttle FPS in running games (prevents 100% CPU workload). menu = 60 ; Throttle FPS in menus only. [profiler2] server = "127.0.0.1" server.port = "8000" ; Use a free port on your machine. server.threads = "6" ; Enough for the browser's parallel connection limit [hotkey] ; Each one of the specified keys will trigger the action on the left ; for multiple-key combinations, separate keys with '+'. ; See keys.txt for the list of key names. ; > SYSTEM SETTINGS exit = "Ctrl+Break", "Super+Q", "Alt+F4" ; Exit to desktop cancel = Escape ; Close or cancel the current dialog box/popup confirm = Return ; Confirm the current command pause = Pause ; Pause/unpause game screenshot = F2 ; Take PNG screenshot bigscreenshot = "Shift+F2" ; Take large BMP screenshot togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots wireframe = "Alt+Shift+W" ; Toggle wireframe mode silhouettes = "Alt+Shift+S" ; Toggle unit silhouettes showsky = "Alt+Z" ; Toggle sky ; > DIALOG HOTKEYS summary = "Ctrl+Tab" ; Toggle in-game summary lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window. structree = "Alt+Shift+T" ; Show structure tree civinfo = "Alt+Shift+H" ; Show civilization info ; > CLIPBOARD CONTROLS copy = "Ctrl+C" ; Copy to clipboard paste = "Ctrl+V" ; Paste from clipboard cut = "Ctrl+X" ; Cut selected text and copy to the clipboard ; > CONSOLE SETTINGS console.toggle = BackQuote, F9 ; Open/close console ; > OVERLAY KEYS fps.toggle = "Alt+F" ; Toggle frame counter realtime.toggle = "Alt+T" ; Toggle current display of computer time timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter -ceasefirecounter.toggle = unused ; Toggle ceasefire counter +ceasefirecounter.toggle = "" ; Toggle ceasefire counter ; > HOTKEYS ONLY chat = Return ; Toggle chat window teamchat = "T" ; Toggle chat window in team chat mode privatechat = "L" ; Toggle chat window and select the previous private chat partner ; > QUICKSAVE quicksave = "Shift+F5" quickload = "Shift+F8" [hotkey.camera] reset = "R" ; Reset camera rotation to default. follow = "F" ; Follow the first unit in the selection -rallypointfocus = unused ; Focus the camera on the rally point of the selected building +rallypointfocus = "" ; Focus the camera on the rally point of the selected building zoom.in = Plus, NumPlus ; Zoom camera in (continuous control) zoom.out = Minus, NumMinus ; Zoom camera out (continuous control) zoom.wheel.in = WheelUp ; Zoom camera in (stepped control) zoom.wheel.out = WheelDown ; Zoom camera out (stepped control) rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control) rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control) pan = MouseMiddle ; Enable scrolling by moving mouse left = A, LeftArrow ; Scroll or rotate left right = D, RightArrow ; Scroll or rotate right up = W, UpArrow ; Scroll or rotate up/forwards down = S, DownArrow ; Scroll or rotate down/backwards scroll.speed.increase = "Ctrl+Shift+S" ; Increase scroll speed scroll.speed.decrease = "Ctrl+Alt+S" ; Decrease scroll speed rotate.speed.increase = "Ctrl+Shift+R" ; Increase rotation speed rotate.speed.decrease = "Ctrl+Alt+R" ; Decrease rotation speed zoom.speed.increase = "Ctrl+Shift+Z" ; Increase zoom speed zoom.speed.decrease = "Ctrl+Alt+Z" ; Decrease zoom speed [hotkey.camera.jump] 1 = F5 ; Jump to position N 2 = F6 3 = F7 4 = F8 ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.camera.jump.set] 1 = "Ctrl+F5" ; Set jump position N 2 = "Ctrl+F6" 3 = "Ctrl+F7" 4 = "Ctrl+F8" ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.profile] toggle = "F11" ; Enable/disable real-time profiler save = "Shift+F11" ; Save current profiler data to logs/profile.txt [hotkey.profile2] toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler [hotkey.selection] cancel = Esc ; Un-select all units and cancel building placement add = Shift ; Add units to selection militaryonly = Alt ; Add only military units to the selection nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection idleonly = "I" ; Select only idle units woundedonly = "O" ; Select only wounded units remove = Ctrl ; Remove units from selection idleworker = Period, NumDecimal ; Select next idle worker idlewarrior = Slash, NumDivide ; Select next idle warrior idleunit = BackSlash ; Select next idle unit offscreen = Alt ; Include offscreen units in selection [hotkey.selection.group.add] 0 = "Shift+0", "Shift+Num0" 1 = "Shift+1", "Shift+Num1" 2 = "Shift+2", "Shift+Num2" 3 = "Shift+3", "Shift+Num3" 4 = "Shift+4", "Shift+Num4" 5 = "Shift+5", "Shift+Num5" 6 = "Shift+6", "Shift+Num6" 7 = "Shift+7", "Shift+Num7" 8 = "Shift+8", "Shift+Num8" 9 = "Shift+9", "Shift+Num9" [hotkey.selection.group.save] 0 = "Ctrl+0", "Ctrl+Num0" 1 = "Ctrl+1", "Ctrl+Num1" 2 = "Ctrl+2", "Ctrl+Num2" 3 = "Ctrl+3", "Ctrl+Num3" 4 = "Ctrl+4", "Ctrl+Num4" 5 = "Ctrl+5", "Ctrl+Num5" 6 = "Ctrl+6", "Ctrl+Num6" 7 = "Ctrl+7", "Ctrl+Num7" 8 = "Ctrl+8", "Ctrl+Num8" 9 = "Ctrl+9", "Ctrl+Num9" [hotkey.selection.group.select] 0 = 0, Num0 1 = 1, Num1 2 = 2, Num2 3 = 3, Num3 4 = 4, Num4 5 = 5, Num5 6 = 6, Num6 7 = 7, Num7 8 = 8, Num8 9 = 9, Num9 [hotkey.gamesetup] mapbrowser.open = "M" [hotkey.session] kill = Delete, Backspace ; Destroy selected units stop = "H" ; Stop the current action backtowork = "Y" ; The unit will go back to work unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected -move = unused ; Modifier to move to a point instead of another action (e.g. gather) +move = "" ; Modifier to move to a point instead of another action (e.g. gather) attack = Ctrl ; Modifier to attack instead of another action (e.g. capture) attackmove = Ctrl ; Modifier to attackmove when clicking on a point attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys) garrison = Ctrl ; Modifier to garrison when clicking on building autorallypoint = Ctrl ; Modifier to set the rally point on the building itself guard = "G" ; Modifier to escort/guard when clicking on unit/building patrol = "P" ; Modifier to patrol a unit repair = "J" ; Modifier to repair when clicking on building/mechanical unit queue = Shift ; Modifier to queue unit orders instead of replacing orderone = Alt ; Modifier to order only one entity in selection. batchtrain = Shift ; Modifier to train units in batches massbarter = Shift ; Modifier to barter bunch of resources masstribute = Shift ; Modifier to tribute bunch of resources noconfirmation = Shift ; Do not ask confirmation when deleting a building/unit fulltradeswap = Shift ; Modifier to put the desired trade resource to 100% unloadtype = Shift ; Modifier to unload all units of type deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting rotate.cw = RightBracket ; Rotate building placement preview clockwise rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise snaptoedges = Ctrl ; Modifier to align new structures with nearby existing structure ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel highlightguarding = PageDown ; Toggle highlight of guarding units highlightguarded = PageUp ; Toggle highlight of guarded units diplomacycolors = "Alt+X" ; Toggle diplomacy colors toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units [hotkey.session.gui] toggle = "Alt+G" ; Toggle visibility of session GUI menu.toggle = "F10" ; Toggle in-game menu diplomacy.toggle = "Ctrl+H" ; Toggle in-game diplomacy page barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel [hotkey.session.savedgames] delete = Delete, Backspace ; Delete the selected saved game asking confirmation noconfirmation = Shift ; Do not ask confirmation when deleting a game [hotkey.session.queueunit] ; > UNIT TRAINING 1 = "Z" ; add first unit type to queue 2 = "X" ; add second unit type to queue 3 = "C" ; add third unit type to queue 4 = "V" ; add fourth unit type to queue 5 = "B" ; add fivth unit type to queue 6 = "N" ; add sixth unit type to queue 7 = "M" ; add seventh unit type to queue 8 = Comma ; add eighth unit type to queue [hotkey.session.timewarp] fastforward = Space ; If timewarp mode enabled, speed up the game rewind = "Shift+Backspace" ; If timewarp mode enabled, go back to earlier point in the game [hotkey.tab] next = "Tab", "Alt+S" ; Show the next tab prev = "Shift+Tab", "Alt+W" ; Show the previous tab [hotkey.text] ; > GUI TEXTBOX HOTKEYS delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor delete.right = "Ctrl+Del" ; Delete word to the right of cursor move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor [gui] cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking) scale = 1.0 ; GUI scaling factor, for improved compatibility with 4K displays [gui.gamesetup] enabletips = true ; Enable/Disable tips during gamesetup (for newcomers) assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled. aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest) aibehavior = "random" ; Default behavior of the AI (random, balanced, aggressive or defensive) settingsslide = true ; Enable/Disable settings panel slide [gui.loadingscreen] progressdescription = false ; Whether to display the progress percent or a textual description [gui.session] camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one? timeelapsedcounter = false ; Show the game duration in the top right corner ceasefirecounter = false ; Show the remaining ceasefire time in the top right corner batchtrainingsize = 5 ; Number of units to be trained per batch by default (when pressing the hotkey) scrollbatchratio = 1 ; Number of times you have to scroll to increase/decrease the batchsize by 1 woundedunithotkeythreshold = 33 ; The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number attackrange = true ; Display attack range overlays of selected defensive structures aurasrange = true ; Display aura range overlays of selected units and structures healrange = true ; Display heal range overlays of selected units rankabovestatusbar = true ; Show rank icons above status bars experiencestatusbar = true ; Show an experience status bar above each selected unit respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending) snaptoedges = "disabled" ; Possible values: disabled, enabled. snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time. defaultformation = "special/formations/box" ; For walking orders, automatically put units into this formation if they don't have one already. [gui.session.minimap] blinkduration = 1.7 ; The blink duration while pinging pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification [gui.session.notifications] attack = true ; Show a chat notification if you are attacked by another player tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode barter = true ; Show a chat notification to observers when a player bartered resources phase = completed ; 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. Possible values: none, completed, all. [gui.splashscreen] enable = true ; Enable/disable the splashscreen version = 0 ; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch [gui.session.diplomacycolors] self = "21 55 149" ; Color of your units when diplomacy colors are enabled ally = "86 180 31" ; Color of allies when diplomacy colors are enabled neutral = "231 200 5" ; Color of neutral players when diplomacy colors are enabled enemy = "150 20 20" ; Color of enemies when diplomacy colors are enabled [joystick] ; EXPERIMENTAL: joystick/gamepad settings enable = false deadzone = 8192 [joystick.camera] pan.x = 0 pan.y = 1 rotate.x = 3 rotate.y = 2 zoom.in = 5 zoom.out = 4 [chat] timestamp = true ; Show at which time chat messages have been sent [chat.session] extended = true ; Whether to display the chat history [lobby] history = 0 ; Number of past messages to display on join room = "arena24" ; Default MUC room to join server = "lobby.wildfiregames.com" ; Address of lobby server tls = true ; Whether to use TLS encryption when connecting to the server. verify_certificate = false ; Whether to reject connecting to the lobby if the TLS certificate is invalid (TODO: wait for Gloox GnuTLS trust implementation to be fixed) terms_url = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/prelobby/common/terms/"; Allows the user to save the text and print the terms terms_of_service = "0" ; Version (hash) of the Terms of Service that the user has accepted terms_of_use = "0" ; Version (hash) of the Terms of Use that the user has accepted privacy_policy = "0" ; Version (hash) of the Privacy Policy that the user has accepted xpartamupp = "wfgbot24" ; Name of the server-side XMPP-account that manage games echelon = "echelon24" ; Name of the server-side XMPP-account that manages ratings buddies = "," ; Comma separated list of playernames that the current user has marked as buddies rememberpassword = true ; Whether to store the encrypted password in the user config [lobby.columns] gamerating = false ; Show the average rating of the participating players in a column of the gamelist [lobby.stun] enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router. ; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding. server = "lobby.wildfiregames.com" ; Address of the STUN server. port = 3478 ; Port of the STUN server. delay = 200 ; Duration in milliseconds that is waited between STUN messages. ; Smaller numbers speed up joins but also become less stable. [mod] enabledmods = "mod public" [modio] public_key = "RWTsHxQMrRq4xwHisyBa2rNQfAedcINzbTT83jeX4/ZcfVxqLfWB4y8w" ; Public key corresponding to the private key valid mods are signed with disclaimer = "0" ; Version (hash) of the Disclaimer that the user has accepted [modio.v1] baseurl = "https://api.mod.io/v1" api_key = "23df258a71711ea6e4b50893acc1ba55" name_id = "0ad" [network] duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join. lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled. observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached [overlay] fps = "false" ; Show frames per second in top right corner realtime = "false" ; Show current system time in top right corner netwarnings = "true" ; Show warnings if the network connection is bad [profiler2] autoenable = false ; Enable HTTP server output at startup (default off for security/performance) gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available [rlinterface] address = "127.0.0.1:6000" [sound] mastergain = 0.9 musicgain = 0.2 ambientgain = 0.6 actiongain = 0.7 uigain = 0.7 [sound.notify] nick = true ; Play a sound when someone mentions your name in the lobby or game gamesetup.join = false ; Play a sound when a new client joins the game setup [tinygettext] debug = false ; Print error messages each time a translation for an English string is not found. [userreport] ; Opt-in online user reporting system url_upload = "https://feedback.wildfiregames.com/report/upload/v1/" ; URL where UserReports are uploaded to url_publication = "https://feedback.wildfiregames.com/" ; URL where UserReports were analyzed and published url_terms = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/userreport/Terms_and_Conditions.txt"; Allows the user to save the text and print the terms terms = "0" ; Version (hash) of the UserReporter Terms that the user has accepted [view] ; Camera control settings scroll.speed = 120.0 scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed rotate.x.speed = 1.2 rotate.x.min = 28.0 rotate.x.max = 60.0 rotate.x.default = 35.0 rotate.y.speed = 2.0 rotate.y.speed.wheel = 0.45 rotate.y.default = 0.0 rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed drag.speed = 0.5 zoom.speed = 256.0 zoom.speed.wheel = 32.0 zoom.min = 50.0 zoom.max = 200.0 zoom.default = 120.0 zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed pos.smoothness = 0.1 zoom.smoothness = 0.4 rotate.x.smoothness = 0.5 rotate.y.smoothness = 0.3 near = 2.0 ; Near plane distance far = 4096.0 ; Far plane distance fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide height.smoothness = 0.5 height.min = 16 Index: ps/trunk/source/ps/ConfigDB.cpp =================================================================== --- ps/trunk/source/ps/ConfigDB.cpp (revision 24617) +++ ps/trunk/source/ps/ConfigDB.cpp (revision 24618) @@ -1,491 +1,496 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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);\ + if (!it->second.empty())\ + 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);\ + if (!it->second.empty())\ + 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()) + if (!name.empty()) { CStr key(header + name); newMap[key] = values; if (g_UnloggedEntries.find(key) != g_UnloggedEntries.end()) LOGMESSAGE("Loaded config string \"%s\"", key); + else if (values.empty()) + LOGMESSAGE("Loaded config string \"%s\" = (empty)", key); else { std::string vals; - for (size_t i = 0; i < newMap[key].size() - 1; ++i) + for (size_t i = 0; i + 1 < newMap[key].size(); ++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) + for (i = 0; i + 1 < p.second.size(); ++i) pos += sprintf(pos, "\"%s\", ", EscapeString(p.second[i]).c_str()); - pos += sprintf(pos, "\"%s\"\n", EscapeString(p.second[i]).c_str()); + if (!p.second.empty()) + pos += sprintf(pos, "\"%s\"\n", EscapeString(p.second[i]).c_str()); + else + pos += sprintf(pos, "\"\"\n"); } 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/Hotkey.cpp =================================================================== --- ps/trunk/source/ps/Hotkey.cpp (revision 24617) +++ ps/trunk/source/ps/Hotkey.cpp (revision 24618) @@ -1,344 +1,353 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "Hotkey.h" #include #include "lib/external_libraries/libsdl.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/ConfigDB.h" #include "ps/Globals.h" #include "ps/KeyName.h" static bool unified[UNIFIED_LAST - UNIFIED_SHIFT]; std::unordered_map g_HotkeyMap; std::unordered_map g_HotkeyStatus; static_assert(std::is_integral::type>::value, "SDL_Scancode is not an integral enum."); static_assert(SDL_USEREVENT_ == SDL_USEREVENT, "SDL_USEREVENT_ is not the same type as the real SDL_USEREVENT"); +static_assert(UNUSED_HOTKEY_CODE == SDL_SCANCODE_UNKNOWN); // Look up each key binding in the config file and set the mappings for // all key combinations that trigger it. static void LoadConfigBindings() { for (const std::pair& configPair : g_ConfigDB.GetValuesWithPrefix(CFG_COMMAND, "hotkey.")) { std::string hotkeyName = configPair.first.substr(7); // strip the "hotkey." prefix - for (const CStr& hotkey : configPair.second) + + if (configPair.second.empty()) { - if (hotkey.LowerCase() == "unused") - continue; + // Unused hotkeys must still be registered in the map to appear in the hotkey editor. + SHotkeyMapping unusedCode; + unusedCode.name = hotkeyName; + unusedCode.negated = false; + g_HotkeyMap[UNUSED_HOTKEY_CODE].push_back(unusedCode); + continue; + } + for (const CStr& hotkey : configPair.second) + { std::vector keyCombination; // Iterate through multiple-key bindings (e.g. Ctrl+I) boost::char_separator sep("+"); typedef boost::tokenizer > tokenizer; tokenizer tok(hotkey, sep); for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it) { // Attempt decode as key name SDL_Scancode scancode = FindScancode(it->c_str()); if (!scancode) { LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey.c_str()); continue; } SKey key = { scancode, false }; keyCombination.push_back(key); } std::vector::iterator itKey, itKey2; for (itKey = keyCombination.begin(); itKey != keyCombination.end(); ++itKey) { SHotkeyMapping bindCode; bindCode.name = hotkeyName; bindCode.negated = itKey->negated; for (itKey2 = keyCombination.begin(); itKey2 != keyCombination.end(); ++itKey2) if (itKey != itKey2) // Push any auxiliary keys bindCode.requires.push_back(*itKey2); g_HotkeyMap[itKey->code].push_back(bindCode); } } } } void LoadHotkeys() { LoadConfigBindings(); // Set up the state of the hotkeys given no key is down. // i.e. find those hotkeys triggered by all negations. for (const std::pair& p : g_HotkeyMap) for (const SHotkeyMapping& hotkey : p.second) { if (!hotkey.negated) continue; bool allNegated = true; for (const SKey& k : hotkey.requires) if (!k.negated) allNegated = false; if (allNegated) g_HotkeyStatus[hotkey.name] = true; } } void UnloadHotkeys() { g_HotkeyMap.clear(); g_HotkeyStatus.clear(); } bool isNegated(const SKey& key) { // Normal keycodes are below EXTRA_KEYS_BASE if ((int)key.code < EXTRA_KEYS_BASE && g_keys[key.code] == key.negated) return false; // Mouse 'keycodes' are after the modifier keys else if ((int)key.code < MOUSE_LAST && (int)key.code > MOUSE_BASE && g_mouse_buttons[key.code - MOUSE_BASE] == key.negated) return false; // Modifier keycodes are between the normal keys and the mouse 'keys' else if ((int)key.code < UNIFIED_LAST && (int)key.code > SDL_NUM_SCANCODES && unified[key.code - UNIFIED_SHIFT] == key.negated) return false; else return true; } InReaction HotkeyStateChange(const SDL_Event_* ev) { if (ev->ev.type == SDL_HOTKEYPRESS) g_HotkeyStatus[static_cast(ev->ev.user.data1)] = true; else if (ev->ev.type == SDL_HOTKEYUP) g_HotkeyStatus[static_cast(ev->ev.user.data1)] = false; return IN_PASS; } InReaction HotkeyInputHandler(const SDL_Event_* ev) { int scancode = SDL_SCANCODE_UNKNOWN; switch(ev->ev.type) { case SDL_KEYDOWN: case SDL_KEYUP: scancode = ev->ev.key.keysym.scancode; break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: // Mousewheel events are no longer buttons, but we want to maintain the order // expected by g_mouse_buttons for compatibility if (ev->ev.button.button >= SDL_BUTTON_X1) scancode = MOUSE_BASE + (int)ev->ev.button.button + 2; else scancode = MOUSE_BASE + (int)ev->ev.button.button; break; case SDL_MOUSEWHEEL: if (ev->ev.wheel.y > 0) { scancode = MOUSE_WHEELUP; break; } else if (ev->ev.wheel.y < 0) { scancode = MOUSE_WHEELDOWN; break; } else if (ev->ev.wheel.x > 0) { scancode = MOUSE_X2; break; } else if (ev->ev.wheel.x < 0) { scancode = MOUSE_X1; break; } return IN_PASS; default: return IN_PASS; } // Somewhat hackish: // Create phantom 'unified-modifier' events when left- or right- modifier keys are pressed // Just send them to this handler; don't let the imaginary event codes leak back to real SDL. SDL_Event_ phantom; phantom.ev.type = ((ev->ev.type == SDL_KEYDOWN) || (ev->ev.type == SDL_MOUSEBUTTONDOWN)) ? SDL_KEYDOWN : SDL_KEYUP; if (phantom.ev.type == SDL_KEYDOWN) phantom.ev.key.repeat = ev->ev.type == SDL_KEYDOWN ? ev->ev.key.repeat : 0; if (scancode == SDL_SCANCODE_LSHIFT || scancode == SDL_SCANCODE_RSHIFT) { phantom.ev.key.keysym.scancode = static_cast(UNIFIED_SHIFT); unified[0] = (phantom.ev.type == SDL_KEYDOWN); HotkeyInputHandler(&phantom); } else if (scancode == SDL_SCANCODE_LCTRL || scancode == SDL_SCANCODE_RCTRL) { phantom.ev.key.keysym.scancode = static_cast(UNIFIED_CTRL); unified[1] = (phantom.ev.type == SDL_KEYDOWN); HotkeyInputHandler(&phantom); } else if (scancode == SDL_SCANCODE_LALT || scancode == SDL_SCANCODE_RALT) { phantom.ev.key.keysym.scancode = static_cast(UNIFIED_ALT); unified[2] = (phantom.ev.type == SDL_KEYDOWN); HotkeyInputHandler(&phantom); } else if (scancode == SDL_SCANCODE_LGUI || scancode == SDL_SCANCODE_RGUI) { phantom.ev.key.keysym.scancode = static_cast(UNIFIED_SUPER); unified[3] = (phantom.ev.type == SDL_KEYDOWN); HotkeyInputHandler(&phantom); } // Check whether we have any hotkeys registered for this particular keycode if (g_HotkeyMap.find(scancode) == g_HotkeyMap.end()) return (IN_PASS); // Inhibit the dispatch of hotkey events caused by real keys (not fake mouse button // events) while the console is up. bool consoleCapture = false; if (g_Console && g_Console->IsActive() && scancode < SDL_NUM_SCANCODES) consoleCapture = true; // Here's an interesting bit: // If you have an event bound to, say, 'F', and another to, say, 'Ctrl+F', pressing // 'F' while control is down would normally fire off both. // To avoid this, set the modifier keys for /all/ events this key would trigger // (Ctrl, for example, is both group-save and bookmark-save) // but only send a HotkeyPress/HotkeyDown event for the event with bindings most precisely // matching the conditions (i.e. the event with the highest number of auxiliary // keys, providing they're all down) bool typeKeyDown = ( ev->ev.type == SDL_KEYDOWN ) || ( ev->ev.type == SDL_MOUSEBUTTONDOWN ) || (ev->ev.type == SDL_MOUSEWHEEL); // -- KEYDOWN SECTION -- std::vector closestMapNames; size_t closestMapMatch = 0; for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode]) { // If a key has been pressed, and this event triggers on its release, skip it. // Similarly, if the key's been released and the event triggers on a keypress, skip it. if (hotkey.negated == typeKeyDown) continue; // Check for no unpermitted keys bool accept = true; for (const SKey& k : hotkey.requires) { accept = isNegated(k); if (!accept) break; } if (accept && !(consoleCapture && hotkey.name != "console.toggle")) { // Check if this is an equally precise or more precise match if (hotkey.requires.size() + 1 >= closestMapMatch) { // Check if more precise if (hotkey.requires.size() + 1 > closestMapMatch) { // Throw away the old less-precise matches closestMapNames.clear(); closestMapMatch = hotkey.requires.size() + 1; } closestMapNames.push_back(hotkey.name.c_str()); } } } for (size_t i = 0; i < closestMapNames.size(); ++i) { // Send a KeyPress event when a key is pressed initially and on mouseButton and mouseWheel events. if (ev->ev.type != SDL_KEYDOWN || ev->ev.key.repeat == 0) { SDL_Event_ hotkeyPressNotification; hotkeyPressNotification.ev.type = SDL_HOTKEYPRESS; hotkeyPressNotification.ev.user.data1 = const_cast(closestMapNames[i]); in_push_priority_event(&hotkeyPressNotification); } // Send a HotkeyDown event on every key, mouseButton and mouseWheel event. // For keys the event is repeated depending on hardware and OS configured interval. // On linux, modifier keys (shift, alt, ctrl) are not repeated, see https://github.com/SFML/SFML/issues/122. SDL_Event_ hotkeyDownNotification; hotkeyDownNotification.ev.type = SDL_HOTKEYDOWN; hotkeyDownNotification.ev.user.data1 = const_cast(closestMapNames[i]); in_push_priority_event(&hotkeyDownNotification); } // -- KEYUP SECTION -- for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode]) { // If it's a keydown event, won't cause HotKeyUps in anything that doesn't // use this key negated => skip them // If it's a keyup event, won't cause HotKeyUps in anything that does use // this key negated => skip them too. if (hotkey.negated != typeKeyDown) continue; // Check for no unpermitted keys bool accept = true; for (const SKey& k : hotkey.requires) { accept = isNegated(k); if (!accept) break; } if (accept) { SDL_Event_ hotkeyNotification; hotkeyNotification.ev.type = SDL_HOTKEYUP; hotkeyNotification.ev.user.data1 = const_cast(hotkey.name.c_str()); in_push_priority_event(&hotkeyNotification); } } return IN_PASS; } bool HotkeyIsPressed(const CStr& keyname) { return g_HotkeyStatus[keyname]; } Index: ps/trunk/source/ps/Hotkey.h =================================================================== --- ps/trunk/source/ps/Hotkey.h (revision 24617) +++ ps/trunk/source/ps/Hotkey.h (revision 24618) @@ -1,85 +1,87 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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_HOTKEY #define INCLUDED_HOTKEY /** * @file * Hotkey system. * * Hotkeys consist of a name (an arbitrary string), and a key mapping. * The names and mappings are loaded from the config system (any * config setting with the name prefix "hotkey."). * When a hotkey is pressed one SDL_HOTKEYPRESS is triggered. While the key is * kept down repeated SDL_HOTKEYDOWN events are triggered at an interval * determined by the OS. When a hotkey is released an SDL_HOTKEYUP event is * triggered. All with the hotkey name stored in ev.user.data1 as a const char*. */ #include "CStr.h" #include "lib/input.h" #include #include // SDL_Scancode is an enum, we'll use an explicit int to avoid including SDL in this header. using SDL_Scancode_ = int; // 0x8000 is SDL_USEREVENT, this is static_asserted in Hotkey.cpp // We do this to avoid including SDL in this header. const uint SDL_USEREVENT_ = 0x8000; const uint SDL_HOTKEYPRESS = SDL_USEREVENT_; const uint SDL_HOTKEYDOWN = SDL_USEREVENT_ + 1; const uint SDL_HOTKEYUP = SDL_USEREVENT_ + 2; +constexpr SDL_Scancode_ UNUSED_HOTKEY_CODE = 0; // == SDL_SCANCODE_UNKNOWN + struct SKey { SDL_Scancode_ code; // scancode or MOUSE_ or UNIFIED_ value bool negated; // whether the key must be pressed (false) or unpressed (true) bool operator<(const SKey& o) const { return code < o.code && negated < o.negated; } bool operator==(const SKey& o) const { return code == o.code && negated == o.negated; } }; // Hotkey data associated with an externally-specified 'primary' keycode struct SHotkeyMapping { CStr name; // name of the hotkey bool negated; // whether the primary key must be pressed (false) or unpressed (true) std::vector requires; // list of non-primary keys that must also be active }; typedef std::vector KeyMapping; // A mapping of scancodes onto the hotkeys that are associated with that key. // (A hotkey triggered by a combination of multiple keys will be in this map // multiple times.) extern std::unordered_map g_HotkeyMap; // The current pressed status of hotkeys extern std::unordered_map g_HotkeyStatus; extern void LoadHotkeys(); extern void UnloadHotkeys(); extern InReaction HotkeyStateChange(const SDL_Event_* ev); extern InReaction HotkeyInputHandler(const SDL_Event_* ev); extern bool HotkeyIsPressed(const CStr& keyname); #endif // INCLUDED_HOTKEY Index: ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp (revision 24617) +++ ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp (revision 24618) @@ -1,171 +1,173 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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_Hotkey.h" #include "lib/external_libraries/libsdl.h" #include "ps/CLogger.h" #include "ps/Hotkey.h" #include "ps/KeyName.h" #include "scriptinterface/ScriptConversions.h" #include #include #include /** * Convert an unordered map to a JS object, mapping keys to values. * Assumes T to have a c_str() method that returns a const char* * NB: this is unordered since no particular effort is made to preserve order. * TODO: this could be moved to ScriptConversions.cpp if the need arises. */ template static void ToJSVal_unordered_map(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map& val) { JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); if (!obj) { ret.setUndefined(); return; } for (const std::pair& item : val) { JS::RootedValue el(rq.cx); ScriptInterface::ToJSVal(rq, &el, item.second); JS_SetProperty(rq.cx, obj, item.first.c_str(), el); } ret.setObject(*obj); } template<> void ScriptInterface::ToJSVal>>>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map>>& val) { ToJSVal_unordered_map(rq, ret, val); } template<> void ScriptInterface::ToJSVal>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map& val) { ToJSVal_unordered_map(rq, ret, val); } /** * @return a (js) object mapping hotkey name (from cfg files) to a list ofscancode names */ JS::Value GetHotkeyMap(ScriptInterface::CmptPrivate* pCmptPrivate) { ScriptRequest rq(*pCmptPrivate->pScriptInterface); JS::RootedValue hotkeyMap(rq.cx); std::unordered_map>> hotkeys; for (const std::pair& key : g_HotkeyMap) for (const SHotkeyMapping& mapping : key.second) { std::vector keymap; - keymap.push_back(FindScancodeName(static_cast(key.first))); + if (key.first != UNUSED_HOTKEY_CODE) + keymap.push_back(FindScancodeName(static_cast(key.first))); for (const SKey& secondary_key : mapping.requires) keymap.push_back(FindScancodeName(static_cast(secondary_key.code))); - // All hotkey permutations are present so only push one (arbitrarily). - if (keymap.size() == 1 || keymap[0] < keymap[1]) + // If keymap is empty (== unused) or size 1, push the combination. + // Otherwise, all permutations of the combination will exist, so pick one using an arbitrary order. + if (keymap.size() < 2 || keymap[0] < keymap[1]) hotkeys[mapping.name].emplace_back(keymap); } pCmptPrivate->pScriptInterface->ToJSVal(rq, &hotkeyMap, hotkeys); return hotkeyMap; } /** * @return a (js) object mapping scancode names to their locale-dependent name. */ JS::Value GetScancodeKeyNames(ScriptInterface::CmptPrivate* pCmptPrivate) { ScriptRequest rq(*pCmptPrivate->pScriptInterface); JS::RootedValue obj(rq.cx); std::unordered_map map; // Get the name of all scancodes. // This is slightly wasteful but should be fine overall, they are dense. for (int i = 0; i < MOUSE_LAST; ++i) map[FindScancodeName(static_cast(i))] = FindKeyName(static_cast(i)); pCmptPrivate->pScriptInterface->ToJSVal(rq, &obj, map); return obj; } void ReloadHotkeys(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { UnloadHotkeys(); LoadHotkeys(); } JS::Value GetConflicts(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue combination) { ScriptInterface* scriptInterface = pCmptPrivate->pScriptInterface; ScriptRequest rq(*scriptInterface); std::vector keys; if (!scriptInterface->FromJSVal(rq, combination, keys)) { LOGERROR("Invalid hotkey combination"); return JS::NullValue(); } if (keys.empty()) return JS::NullValue(); // Pick a random code as a starting point of the hotkeys (they are all equivalent). SDL_Scancode_ startCode = FindScancode(keys.back()); std::unordered_map::const_iterator it = g_HotkeyMap.find(startCode); if (it == g_HotkeyMap.end()) return JS::NullValue(); // Create a sorted vector with the remaining keys. keys.pop_back(); std::set codes; for (const std::string& key : keys) codes.insert(SKey{ FindScancode(key), false }); std::vector conflicts; // This isn't very efficient, but we shouldn't iterate too many hotkeys // since we at least have one matching key. for (const SHotkeyMapping& keymap : it->second) { std::set match(keymap.requires.begin(), keymap.requires.end()); if (codes == match) conflicts.emplace_back(keymap.name); } if (conflicts.empty()) return JS::NullValue(); JS::RootedValue ret(rq.cx); scriptInterface->ToJSVal(rq, &ret, conflicts); return ret; } void JSI_Hotkey::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("GetHotkeyMap"); scriptInterface.RegisterFunction("GetScancodeKeyNames"); scriptInterface.RegisterFunction("ReloadHotkeys"); scriptInterface.RegisterFunction("GetConflicts"); } Index: ps/trunk/source/ps/tests/test_ConfigDB.h =================================================================== --- ps/trunk/source/ps/tests/test_ConfigDB.h (nonexistent) +++ ps/trunk/source/ps/tests/test_ConfigDB.h (revision 24618) @@ -0,0 +1,87 @@ +/* Copyright (C) 2021 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 "lib/self_test.h" + +#include "lib/file/vfs/vfs.h" +#include "ps/ConfigDB.h" + +extern PIVFS g_VFS; + +class TestConfigDB : public CxxTest::TestSuite +{ + CConfigDB* configDB; +public: + + void setUp() + { + g_VFS = CreateVfs(); + TS_ASSERT_OK(g_VFS->Mount(L"config", DataDir()/"_testconfig")); + + configDB = new CConfigDB; + } + + void tearDown() + { + DeleteDirectory(DataDir()/"_testconfig"); + g_VFS.reset(); + + delete configDB; + } + + void test_setting_int() + { + configDB->SetConfigFile(CFG_SYSTEM, "config/file.cfg"); + configDB->WriteFile(CFG_SYSTEM); + configDB->Reload(CFG_SYSTEM); + configDB->SetValueString(CFG_SYSTEM, "test_setting", "5"); + configDB->WriteFile(CFG_SYSTEM); + configDB->Reload(CFG_SYSTEM); + { + std::string res; + configDB->GetValue(CFG_SYSTEM, "test_setting", res); + TS_ASSERT_EQUALS(res, "5"); + } + { + int res; + configDB->GetValue(CFG_SYSTEM, "test_setting", res); + TS_ASSERT_EQUALS(res, 5); + } + } + + void test_setting_empty() + { + configDB->SetConfigFile(CFG_SYSTEM, "config/file.cfg"); + configDB->WriteFile(CFG_SYSTEM); + configDB->Reload(CFG_SYSTEM); + configDB->SetValueList(CFG_SYSTEM, "test_setting", {}); + configDB->WriteFile(CFG_SYSTEM); + configDB->Reload(CFG_SYSTEM); + { + std::string res = "toto"; + configDB->GetValue(CFG_SYSTEM, "test_setting", res); + // Empty config values don't overwrite + TS_ASSERT_EQUALS(res, "toto"); + } + { + int res = 3; + configDB->GetValue(CFG_SYSTEM, "test_setting", res); + // Empty config values don't overwrite + TS_ASSERT_EQUALS(res, 3); + } + } +}; Property changes on: ps/trunk/source/ps/tests/test_ConfigDB.h ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property