From c529d5688fe2b0de56adb56b9e4ecc4d97ff8319 Mon Sep 17 00:00:00 2001 From: Force67 Date: Wed, 12 Jan 2022 22:09:04 +0100 Subject: [PATCH] cvar system --- Code/base/IniSettingsProvider.cpp | 67 ++++++----- Code/base/Setting.h | 86 ++++++++++---- Code/base/simpleini/SimpleIni.h | 2 +- Code/server/GameServer.cpp | 76 ++++++------ Code/server/GameServer.h | 9 +- Code/server/Services/ServerListService.cpp | 2 +- Code/server/main.cpp | 127 ++++++++------------- 7 files changed, 195 insertions(+), 174 deletions(-) diff --git a/Code/base/IniSettingsProvider.cpp b/Code/base/IniSettingsProvider.cpp index 8d6b411dc..7fb68d0dd 100644 --- a/Code/base/IniSettingsProvider.cpp +++ b/Code/base/IniSettingsProvider.cpp @@ -6,8 +6,8 @@ #include #include -#include #include +#include #include @@ -24,34 +24,29 @@ static void ShittyFileWrite(const std::filesystem::path& path, const std::string } template -static SI_Error SetIniValue(CSimpleIni& ini, const T* a_pSection, const T* a_pKey, TVal a_nValue, +static SI_Error SetIniValue(CSimpleIni& ini, const T* a_pSection, const T* a_pKey, const TVal a_nValue, const T* a_pComment = nullptr) { - if (!a_pSection || !a_pKey) - return SI_FAIL; - - // TODO: switch to to_chars - auto buf = fmt::format("{}", a_nValue); + char szValue[64]{}; + std::to_chars(szValue, szValue + sizeof(szValue), a_nValue); // convert to output text T szOutput[256]; CSimpleIni::Converter c(ini.IsUnicode()); - c.ConvertFromStore(buf.c_str(), buf.length() + 1, szOutput, sizeof(szOutput) / sizeof(T)); + c.ConvertFromStore(szValue, std::strlen(szValue) + 1, szOutput, sizeof(szOutput) / sizeof(T)); - // actually add it return ini.AddEntry(a_pSection, a_pKey, szOutput, a_pComment, false, true); } template TVal GetIniValue(CSimpleIni& ini, const T* a_pSection, const T* a_pKey, const TVal a_nDefault, bool& a_pHasMultiple) { - // return the default if we don't have a value const T* pszValue = ini.GetValue(a_pSection, a_pKey, nullptr, &a_pHasMultiple); if (!pszValue || !*pszValue) return a_nDefault; // convert to UTF-8/MBCS which for a numeric value will be the same as ASCII - char szValue[64] = {0}; + char szValue[64]{}; CSimpleIni::Converter c(ini.IsUnicode()); if (!c.ConvertToStore(pszValue, szValue, sizeof(szValue))) { @@ -67,7 +62,7 @@ TVal GetIniValue(CSimpleIni& ini, const T* a_pSection, const T* a_pKey, const TV return a_nDefault; } -std::pair SplitSection(const SettingBase *setting) +std::pair SplitSection(const SettingBase* setting) { std::string nameProperty(setting->name); size_t pos = nameProperty.find_first_of(':'); @@ -77,7 +72,7 @@ std::pair SplitSection(const SettingBase *setting) return {section, name}; } -} +} // namespace void SaveSettingsToIni(const std::filesystem::path& path) { @@ -88,30 +83,30 @@ void SaveSettingsToIni(const std::filesystem::path& path) SettingBase::VisitAll([&](SettingBase* setting) { auto items = SplitSection(setting); auto& section = items.first; - auto name = items.second; + auto& name = items.second; switch (setting->type) { case SettingBase::Type::kBoolean: - error = ini.SetBoolValue(section.c_str(), name, setting->data.as_boolean); + error = ini.SetBoolValue(section.c_str(), name.c_str(), setting->data.as_boolean); break; case SettingBase::Type::kInt: - error = SetIniValue(ini, section.c_str(), name, setting->data.as_int32); + error = SetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_int32); break; case SettingBase::Type::kUInt: - error = SetIniValue(ini, section.c_str(), name, setting->data.as_uint32); + error = SetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_uint32); break; case SettingBase::Type::kInt64: - error = SetIniValue(ini, section.c_str(), name, setting->data.as_int64); + error = SetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_int64); break; case SettingBase::Type::kUInt64: - error = SetIniValue(ini, section.c_str(), name, setting->data.as_uint64); + error = SetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_uint64); break; case SettingBase::Type::kFloat: - error = SetIniValue(ini, section.c_str(), name, setting->data.as_float); + error = SetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_float); break; case SettingBase::Type::kString: - error = ini.SetValue(section.c_str(), name, setting->c_str()); + error = ini.SetValue(section.c_str(), name.c_str(), setting->c_str()); break; default: BASE_ASSERT(true, "SaveSettingsToIni(): Unknown type index for {}", setting->name); @@ -141,33 +136,43 @@ void LoadSettingsFromIni(const std::filesystem::path& path) SettingBase::VisitAll([&](SettingBase* setting) { auto items = SplitSection(setting); auto& section = items.first; - auto name = items.second; + auto& name = items.second; bool multiMatch = false; switch (setting->type) { + // With scalar types we don't expect the size to change... + // However, they should all call StoreValue in the future. case SettingBase::Type::kBoolean: - setting->data.as_boolean = ini.GetBoolValue(section.c_str(), name, setting->data.as_boolean); + setting->data.as_boolean = ini.GetBoolValue(section.c_str(), name.c_str(), setting->data.as_boolean); break; case SettingBase::Type::kInt: - setting->data.as_int32 = GetIniValue(ini, section.c_str(), name, setting->data.as_int32, multiMatch); + setting->data.as_int32 = + GetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_int32, multiMatch); break; case SettingBase::Type::kUInt: - setting->data.as_uint32 = GetIniValue(ini, section.c_str(), name, setting->data.as_uint32, multiMatch); + setting->data.as_uint32 = + GetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_uint32, multiMatch); break; case SettingBase::Type::kInt64: - setting->data.as_int64 = GetIniValue(ini, section.c_str(), name, setting->data.as_int64, multiMatch); + setting->data.as_int64 = + GetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_int64, multiMatch); break; case SettingBase::Type::kUInt64: - setting->data.as_uint64 = GetIniValue(ini, section.c_str(), name, setting->data.as_uint64, multiMatch); + setting->data.as_uint64 = + GetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_uint64, multiMatch); break; case SettingBase::Type::kFloat: - setting->data.as_float = GetIniValue(ini, section.c_str(), name, setting->data.as_float, multiMatch); + setting->data.as_float = + GetIniValue(ini, section.c_str(), name.c_str(), setting->data.as_float, multiMatch); break; - case SettingBase::Type::kString: - // TODO: string_storage - //setting->data.as_string = GetIniValue(ini, section.c_str(), name, setting->data.as_float, multiMatch); + // Strings however are a special case, as it has its own allocator. + case SettingBase::Type::kString: { + // This is not nearly how i want it to be :/ + const char* c = ini.GetValue(section.c_str(), name.c_str(), setting->c_str()); + static_cast(setting)->StoreValue(*setting, c); break; + } default: BASE_ASSERT(true, "LoadSettingsFromIni(): Unknown type index for {}", setting->name); break; diff --git a/Code/base/Setting.h b/Code/base/Setting.h index a9bb04bf0..d16f0a866 100644 --- a/Code/base/Setting.h +++ b/Code/base/Setting.h @@ -2,8 +2,8 @@ // For licensing information see LICENSE at the root of this distribution. #pragma once -#include #include +#include namespace base { @@ -32,12 +32,12 @@ struct SettingBase kLocked = 1 << 1, }; - SettingBase(SettingBase*& parent) : next(parent) + SettingBase(SettingBase*& parent, const char* n, const char* d, Type t) : next(parent), name(n), desc(d), type(t) { parent = this; } - SettingBase() : SettingBase(ROOT()) + explicit SettingBase(const char* n, const char* d, Type t) : SettingBase(ROOT(), n, d, t) { } @@ -72,15 +72,12 @@ struct SettingBase return Type::kFloat; if constexpr (std::is_same_v) return Type::kString; - if constexpr (std::is_same_v) - return Type::kString; - return Type::kNone; } const char* c_str() const { - BASE_ASSERT(type == Type::kString, "Must be a string"); + //BASE_ASSERT(type == Type::kString, "Must be a string"); return data.as_string; } @@ -91,6 +88,7 @@ struct SettingBase // descriptor const char* name{nullptr}; const char* desc{nullptr}; + // Gets aligned to 8 bytes anyway size_t dataLength; union { @@ -107,30 +105,59 @@ struct SettingBase SettingBase* next; }; -// TODO: this would really benefit from CXX 20 requires keyword -template struct Setting : SettingBase +namespace detail { - // This could deserve a constinit - Setting(const char* acName, const char* acDesc, const T acDefault) +template struct FixedStorage +{ + constexpr FixedStorage(SettingBase& b, const T acValue) { - SettingBase::name = acName; - SettingBase::desc = acDesc; + b.dataLength = sizeof(T); + b.data.as_int64 = 0; + + StoreValue(b, acValue); + } - if constexpr (std::is_pointer()) - SettingBase::dataLength = std::strlen(acDefault); - else - SettingBase::dataLength = sizeof(Type); + inline constexpr void StoreValue(SettingBase& b, const T acValue) + { + // The Size is never gonna change, so we only need to update the actual + // data + std::memcpy(&b.data.as_int64, &acValue, sizeof(T)); + } +}; - SettingBase::type = ToTypeIndex(); - data.as_int64 = 0; +template struct DynamicStringStorage +{ + DynamicStringStorage(SettingBase& b, const T* acValue) + { + StoreValue(b, acValue); + } - // poor mans bit cast - std::memcpy(&SettingBase::data.as_int64, &acDefault, sizeof(T)); + inline void StoreValue(SettingBase& b, const T* acValue) + { + m_data = acValue; + b.dataLength = m_data.length() * sizeof(T); + b.data.as_string = m_data.c_str(); } + private: + std::basic_string, StlAllocator> m_data; +}; +} + +// Settings can have their own custom storage spaces. +// However, make sure to make the providers aware of this. +template > class Setting : public SettingBase, public TStorage +{ + public: + Setting(const char* acName, const char* acDesc, const T acDefault) + : SettingBase(acName, acDesc, ToTypeIndex()), TStorage(*this, acDefault) + { + } + + Setting() = delete; + T& value() { - // TODO: bitcast? return reinterpret_cast(data.as_uint64); } @@ -139,10 +166,23 @@ template struct Setting : SettingBase return static_cast(data.as_uint64); } - template ::type> operator bool() + operator bool() { + static_assert(std::is_same_v, "Must be a boolean"); return data.as_boolean; } + + bool empty() + { + return dataLength == 0; + } + + void operator=(const T value) + { + TStorage::StoreValue(*this, value); + } }; +using StringSetting = Setting>; +// NOTE: Wide strings are not supported, since our ini cant handle them. } // namespace base diff --git a/Code/base/simpleini/SimpleIni.h b/Code/base/simpleini/SimpleIni.h index 44dc83d77..acbe838ef 100644 --- a/Code/base/simpleini/SimpleIni.h +++ b/Code/base/simpleini/SimpleIni.h @@ -1296,7 +1296,7 @@ CSimpleIniTempl::CSimpleIniTempl( , m_bStoreIsUtf8(a_bIsUtf8) , m_bAllowMultiKey(a_bAllowMultiKey) , m_bAllowMultiLine(a_bAllowMultiLine) - , m_bSpaces(true) + , m_bSpaces(false) , m_nOrder(0) { } diff --git a/Code/server/GameServer.cpp b/Code/server/GameServer.cpp index 565087c55..ea7082afb 100644 --- a/Code/server/GameServer.cpp +++ b/Code/server/GameServer.cpp @@ -25,49 +25,59 @@ #include #endif -static base::Setting uServerPort{"general:uPort", "Which port to host the server on", 10578u}; -static base::Setting bPremiumTickrate{"general:bPremium", "Use premium tick rate", true}; -static base::Setting sStartESMS{"archive:sMasterFiles", "Master files to use", "Test.esm"}; +static base::Setting uServerPort{"GameServer:uPort", "Which port to host the server on", 10578u}; +static base::Setting bPremiumTickrate{"GameServer:bPremiumMode", "Use premium tick rate", true}; +static base::StringSetting sServerName{"GameServer:sServerName", "Name that shows up in the server list", "Dedicated Together Server"}; +static base::StringSetting sServerDesc{"GameServer:sServerDesc", "Description that shows up in the server list", "Hello there!"}; +static base::StringSetting sServerIconURL{"GameServer:sIconUrl", "URL to the image that shows up in the server list", ""}; +static base::StringSetting sAdminPassword{"GameServer:sAdminPassword", "Admin authentication password", ""}; +static base::StringSetting sToken{"GameServer:sToken", "Admin token", ""}; + +static uint16_t GetUserTickRate() +{ + return bPremiumTickrate ? 60 : 20; +} GameServer* GameServer::s_pInstance = nullptr; -GameServer::GameServer(String aName, String aToken, String aAdminPassword) noexcept - : m_lastFrameTime(std::chrono::high_resolution_clock::now()), m_token(std::move(aToken)), - m_adminPassword(std::move(aAdminPassword)), +GameServer::GameServer() noexcept + : m_lastFrameTime(std::chrono::high_resolution_clock::now()), m_requestStop(false) { BASE_ASSERT(s_pInstance == nullptr, "Server instance already exists?"); s_pInstance = this; - const uint16_t userTick = bPremiumTickrate ? 60 : 20; - auto port = uServerPort.value_as(); - while (!Host(port, userTick)) + while (!Host(port, GetUserTickRate())) { spdlog::warn("Port {} is already in use, trying {}", port, port + 1); port++; } - // Fill in Info field. - m_info.name = std::move(aName); - m_info.desc = "This is my very own ST server"; - m_info.tick_rate = userTick; - m_info.token_url = ""; - - if (m_info.name.empty()) - m_info.name = "A Skyrim Together Server"; - + UpdateInfo(); spdlog::info("Server started on port {}", GetPort()); UpdateTitle(); m_pWorld = std::make_unique(); + BindMessageHandlers(); +} - auto handlerGenerator = [this](auto& x) - { +GameServer::~GameServer() +{ + s_pInstance = nullptr; +} + +void GameServer::Initialize() +{ + m_pWorld->GetScriptService().Initialize(); +} + +void GameServer::BindMessageHandlers() +{ + auto handlerGenerator = [this](auto& x) { using T = typename std::remove_reference_t::Type; m_messageHandlers[T::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) { - auto* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(aConnectionId); if (!pPlayer) @@ -88,7 +98,8 @@ GameServer::GameServer(String aName, String aToken, String aAdminPassword) noexc ClientMessageFactory::Visit(handlerGenerator); // Override authentication request - m_messageHandlers[AuthenticationRequest::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) { + m_messageHandlers[AuthenticationRequest::Opcode] = [this](UniquePtr& apMessage, + ConnectionId_t aConnectionId) { const auto pRealMessage = CastUnique(std::move(apMessage)); HandleAuthenticationRequest(aConnectionId, pRealMessage); }; @@ -96,8 +107,8 @@ GameServer::GameServer(String aName, String aToken, String aAdminPassword) noexc auto adminHandlerGenerator = [this](auto& x) { using T = typename std::remove_reference_t::Type; - m_adminMessageHandlers[T::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) { - + m_adminMessageHandlers[T::Opcode] = [this](UniquePtr& apMessage, + ConnectionId_t aConnectionId) { const auto pRealMessage = CastUnique(std::move(apMessage)); m_pWorld->GetDispatcher().trigger(AdminPacketEvent(pRealMessage.get(), aConnectionId)); }; @@ -108,14 +119,13 @@ GameServer::GameServer(String aName, String aToken, String aAdminPassword) noexc ClientAdminMessageFactory::Visit(adminHandlerGenerator); } -GameServer::~GameServer() +void GameServer::UpdateInfo() { - s_pInstance = nullptr; -} - -void GameServer::Initialize() -{ - m_pWorld->GetScriptService().Initialize(); + // Update Info fields from user facing cvars. + m_info.name = sServerName.c_str(); + m_info.desc = sServerDesc.c_str(); + m_info.icon_url = sServerIconURL.c_str(); + m_info.tick_rate = GetUserTickRate(); } void GameServer::OnUpdate() @@ -309,7 +319,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, info.m_addrRemote.ToString(remoteAddress, 48, false); - if(acRequest->Token == m_token) + if (acRequest->Token == sToken.value()) { auto& scripts = m_pWorld->GetScriptService(); @@ -391,7 +401,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, m_pWorld->GetDispatcher().trigger(PlayerJoinEvent(pPlayer)); } - else if (acRequest->Token == m_adminPassword && !m_adminPassword.empty()) + else if (acRequest->Token == sAdminPassword.value() && !sAdminPassword.empty()) { /* AdminSessionOpen response; Send(aConnectionId, response); diff --git a/Code/server/GameServer.h b/Code/server/GameServer.h index 661a7450d..8c7509f52 100644 --- a/Code/server/GameServer.h +++ b/Code/server/GameServer.h @@ -18,16 +18,18 @@ struct GameServer final : Server { String name; String desc; - String token_url; + String icon_url; uint16_t tick_rate; }; - GameServer(String aName, String aToken, String aAdminPassword = "") noexcept; + GameServer() noexcept; virtual ~GameServer(); TP_NOCOPYMOVE(GameServer); void Initialize(); + void BindMessageHandlers(); + void UpdateInfo(); void OnUpdate() override; void OnConsume(const void* apData, uint32_t aSize, ConnectionId_t aConnectionId) override; @@ -69,9 +71,6 @@ struct GameServer final : Server std::function&, ConnectionId_t)> m_adminMessageHandlers[kClientAdminOpcodeMax]; Info m_info{}; - String m_token; - String m_adminPassword; - std::unique_ptr m_pWorld; Set m_adminSessions; diff --git a/Code/server/Services/ServerListService.cpp b/Code/server/Services/ServerListService.cpp index d9de7e623..f41513877 100644 --- a/Code/server/Services/ServerListService.cpp +++ b/Code/server/Services/ServerListService.cpp @@ -64,7 +64,7 @@ void ServerListService::Announce() const noexcept auto pc = static_cast(m_world.GetPlayerManager().Count()); const auto& cInfo = pServer->GetInfo(); - auto f = std::async(std::launch::async, PostAnnouncement, cInfo.name, cInfo.desc, cInfo.token_url, + auto f = std::async(std::launch::async, PostAnnouncement, cInfo.name, cInfo.desc, cInfo.icon_url, pServer->GetPort(), cInfo.tick_rate, pc, kPlayerMaxCap); } diff --git a/Code/server/main.cpp b/Code/server/main.cpp index b4c6cd331..1135ea4bd 100644 --- a/Code/server/main.cpp +++ b/Code/server/main.cpp @@ -29,102 +29,69 @@ constexpr char kSettingsFileName[] = namespace fs = std::filesystem; -static std::shared_ptr InitLogging() +static base::StringSetting sLogLevel{"sLogLevel", "Log level to print", "info"}; + +struct LogScope { - std::error_code ec; - fs::create_directory("logs", ec); + LogScope() + { + std::error_code ec; + fs::create_directory("logs", ec); - auto rotatingLogger = - std::make_shared("logs/tp_game_server.log", 1048576 * 5, 3); - auto console = std::make_shared(); - console->set_pattern("%^[%H:%M:%S] [%l]%$ %v"); + auto rotatingLogger = + std::make_shared("logs/tp_game_server.log", 1048576 * 5, 3); + auto console = std::make_shared(); + console->set_pattern("%^[%H:%M:%S] [%l]%$ %v"); - auto logger = std::make_shared("", spdlog::sinks_init_list{console, rotatingLogger}); - set_default_logger(logger); - return logger; -} + auto logger = std::make_shared("", spdlog::sinks_init_list{console, rotatingLogger}); + logger->set_level(spdlog::level::from_str(sLogLevel.value())); + set_default_logger(logger); + } -static void ParseSettingsList() -{ - auto configPath = fs::current_path() / kSettingsFileName; - if (!fs::exists(configPath)) + ~LogScope() { - // Write defaults - base::SaveSettingsToIni(configPath); - return; + spdlog::shutdown(); } +}; - base::LoadSettingsFromIni(configPath); -} - -int main(int argc, char** argv) +struct SettingsScope { - auto logger = InitLogging(); - ParseSettingsList(); - - cxxopts::Options options(argv[0], "Game server for " -#if SKYRIM - "Skyrim" -#elif FALLOUT4 - "Fallout 4" -#else - "Unknown game" -#endif - ); - - uint16_t port = 10578; - bool premium = false; - std::string name, token, logLevel, adminPassword; - - options.add_options()("p,port", "port to run on", cxxopts::value(port)->default_value("10578"), "N")( - "root_password", "Admin password", cxxopts::value<>(adminPassword)->default_value(""), - "N")("premium", "Use the premium tick rates", cxxopts::value(premium)->default_value("false"), - "true/false")("h,help", "Display the help message")( - "n,name", "Name to advertise to the public server list", - cxxopts::value<>(name))("l,log", "Log level.", cxxopts::value<>(logLevel)->default_value("info"), - "trace/debug/info/warning/error/critical/off")( - "t,token", "The token required to connect to the server, acts as a password", cxxopts::value<>(token)); - - try + SettingsScope() { - const auto result = options.parse(argc, argv); - - logger->set_level(spdlog::level::from_str(logLevel)); - - if (result.count("help")) + m_Path = fs::current_path() / kSettingsFileName; + if (!fs::exists(m_Path)) { - std::cout << options.help({""}) << std::endl; - return 0; + // Write defaults + base::SaveSettingsToIni(m_Path); + return; } - if (!token.empty() && !name.empty()) - { - throw std::runtime_error("A named server cannot have a token set!"); - } - - if (!token.empty() && token == adminPassword) - { - throw std::runtime_error("The root password cannot be the same as the token!"); - } - - GameServer server(name.c_str(), token.c_str(), adminPassword.c_str()); - // things that need initialization post construction - server.Initialize(); - - while (server.IsListening()) - server.Update(); - } - catch (const cxxopts::OptionException& e) - { - std::cout << "Options parse error: " << e.what() << std::endl; - return -1; + base::LoadSettingsFromIni(m_Path); } - catch (const std::runtime_error& e) + + ~SettingsScope() { - spdlog::error(e.what()); + base::SaveSettingsToIni(m_Path); } - spdlog::shutdown(); +private: + fs::path m_Path; +}; + +int main(int argc, char** argv) +{ + SettingsScope sscope; + (void)sscope; + + LogScope lscope; + (void)lscope; + + GameServer server; + // things that need initialization post construction + server.Initialize(); + + while (server.IsListening()) + server.Update(); return 0; }