Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg +++ ps/trunk/binaries/data/config/default.cfg @@ -176,7 +176,7 @@ 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 @@ -190,7 +190,7 @@ [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) @@ -296,7 +296,7 @@ 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) Index: ps/trunk/source/ps/ConfigDB.cpp =================================================================== --- ps/trunk/source/ps/ConfigDB.cpp +++ ps/trunk/source/ps/ConfigDB.cpp @@ -1,4 +1,4 @@ -/* 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 @@ -99,7 +99,8 @@ 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)\ @@ -107,7 +108,8 @@ 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;\ }\ }\ @@ -384,23 +386,23 @@ 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(); @@ -436,9 +438,12 @@ { 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(); Index: ps/trunk/source/ps/Hotkey.h =================================================================== --- ps/trunk/source/ps/Hotkey.h +++ ps/trunk/source/ps/Hotkey.h @@ -1,4 +1,4 @@ -/* 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 @@ -47,6 +47,8 @@ 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 Index: ps/trunk/source/ps/Hotkey.cpp =================================================================== --- ps/trunk/source/ps/Hotkey.cpp +++ ps/trunk/source/ps/Hotkey.cpp @@ -1,4 +1,4 @@ -/* 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 @@ -35,6 +35,7 @@ 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. @@ -43,11 +44,19 @@ 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) Index: ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp +++ ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp @@ -1,4 +1,4 @@ -/* 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 @@ -79,11 +79,13 @@ 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); Index: ps/trunk/source/ps/tests/test_ConfigDB.h =================================================================== --- ps/trunk/source/ps/tests/test_ConfigDB.h +++ ps/trunk/source/ps/tests/test_ConfigDB.h @@ -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); + } + } +};