diff --git a/Dockerfile b/Dockerfile index ca27c6bf5..3e1d98f57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,17 +8,17 @@ RUN apt-get -y update && apt-get -y upgrade \ && apt-get -y install software-properties-common \ && apt-get -y install wget \ # GCC stuff - && apt-get -y install build-essential gcc-14 g++-14 \ + && apt-get -y install build-essential gcc-15 g++-15 \ # Clang stuff #&& wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc >/dev/null \ #&& add-apt-repository -y 'deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main' \ #&& apt update \ #&& apt-get -y install clang-18 \ - && update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-14 100 \ - && update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-14 100 \ - && update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 \ - && update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 100 \ - && apt-get -y install libstdc++-14-dev \ + && update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-15 100 \ + && update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-15 100 \ + && update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 100 \ + && update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 100 \ + && apt-get -y install libstdc++-15-dev \ && apt-get -y install cmake \ && apt-get -y install git \ # Install required library packages diff --git a/README.md b/README.md index 9e0fcb947..526848c04 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ Any compiler version equal or greater than the supported version should be capab | | Supported | Minimum | |-------|:-----------:|:-----------:| | MSVC | 19.30 (VS2022) | 19.30 (VS2022) | -| Clang | 18 | 18 | -| GCC | 14 | 14 | +| Clang | 21 | 21 | +| GCC | 16 | 15 | > [!NOTE] > Minimum versions support all language features required to build the project but will not receive any fixes to support their continued use (e.g. compiler-specific workarounds). diff --git a/src/account/Service.cpp b/src/account/Service.cpp index 8456665d7..c5252fb82 100644 --- a/src/account/Service.cpp +++ b/src/account/Service.cpp @@ -30,9 +30,9 @@ namespace opts = boost::program_options; namespace ember::account { -Service::Service(log::Logger& logger, commands::PrefixedRegistry& cmd_register) +Service::Service(log::Logger& logger, commands::Registry& registry) : logger(logger) - , cmd_register(cmd_register) + , registry(registry) , start_time(std::chrono::steady_clock::now()) , service(BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO) {} diff --git a/src/account/Service.h b/src/account/Service.h index 150add01c..f5e71b6a1 100644 --- a/src/account/Service.h +++ b/src/account/Service.h @@ -9,7 +9,7 @@ #pragma once #include "ServiceContext.h" -#include +#include #include #include #include @@ -23,7 +23,7 @@ constexpr cstring_view app_name { "Account Daemon" }; class Service { log::Logger& logger; - commands::PrefixedRegistry& cmd_register; + commands::Registry& registry; std::chrono::steady_clock::time_point start_time; boost::asio::io_context service; ServiceContext context; @@ -34,7 +34,7 @@ class Service { public: static boost::program_options::options_description options(); - Service(log::Logger& logger, commands::PrefixedRegistry& cmd_register); + Service(log::Logger& logger, commands::Registry& registry); ~Service() { stop(); diff --git a/src/account/main.cpp b/src/account/main.cpp index 12e3d83b5..eedcd9e84 100644 --- a/src/account/main.cpp +++ b/src/account/main.cpp @@ -9,7 +9,6 @@ #include "Service.h" #include #include -#include #include #include #include @@ -29,7 +28,7 @@ using namespace ember; namespace opts = boost::program_options; opts::variables_map parse_arguments(int argc, const char* argv[]); -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register); +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry); /* * We want to do the minimum amount of work required to get @@ -52,11 +51,10 @@ int main(int argc, const char* argv[]) try { LOG_DEBUG_SYNC(logger, "Registering command handlers..."); commands::Registry registry; - commands::PrefixedRegistry cmd_register(registry); utility::register_command_handlers(registry, logger); utility::register_shared_commands(registry, logger); - const auto ret = run(args, logger, cmd_register); + const auto ret = run(args, logger, registry); LOG_INFO_SYNC(logger, "{} terminated (returned '{}')", account::app_name, ret); return ret; } catch(const std::exception& e) { @@ -64,11 +62,11 @@ int main(int argc, const char* argv[]) try { return EXIT_FAILURE; } -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register) { +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry) { boost::asio::io_context io_ctx; boost::asio::signal_set signals(io_ctx, SIGINT, SIGTERM); - account::Service service(logger, cmd_register); + account::Service service(logger, registry); signals.async_wait([&](auto ec, auto signal) { if(ec) { diff --git a/src/character/Service.h b/src/character/Service.h index 4b5e4f7eb..8d37e51e0 100644 --- a/src/character/Service.h +++ b/src/character/Service.h @@ -9,7 +9,7 @@ #pragma once #include -#include +#include #include #include #include @@ -27,17 +27,17 @@ class Service { std::binary_semaphore stop_flag { 0 }; log::Logger& logger; - commands::PrefixedRegistry& cmd_register; + commands::Registry& registry; std::chrono::steady_clock::time_point start_time; void launch(const boost::program_options::variables_map& args, boost::asio::io_context& service); public: static boost::program_options::options_description options(); - explicit Service(log::Logger& logger, commands::PrefixedRegistry& cmd_register) - : logger(logger), - cmd_register(cmd_register), - start_time(std::chrono::steady_clock::now()) {} + explicit Service(log::Logger& logger, commands::Registry& registry) + : logger(logger) + , registry(registry) + , start_time(std::chrono::steady_clock::now()) {} ~Service() { stop(); diff --git a/src/character/main.cpp b/src/character/main.cpp index 215b462bc..351d007bf 100644 --- a/src/character/main.cpp +++ b/src/character/main.cpp @@ -9,7 +9,6 @@ #include "Service.h" #include #include -#include #include #include #include @@ -29,7 +28,7 @@ using namespace ember; namespace opts = boost::program_options; opts::variables_map parse_arguments(int argc, const char* argv[]); -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register); +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry); /* * We want to do the minimum amount of work required to get @@ -52,11 +51,10 @@ int main(int argc, const char* argv[]) try { LOG_DEBUG_SYNC(logger, "Registering command handlers..."); commands::Registry registry; - commands::PrefixedRegistry cmd_register(registry); utility::register_command_handlers(registry, logger); utility::register_shared_commands(registry, logger); - const auto ret = run(args, logger, cmd_register); + const auto ret = run(args, logger, registry); LOG_INFO_SYNC(logger, "{} terminated (returned '{}')", character::app_name, ret); return ret; } catch(const std::exception& e) { @@ -64,7 +62,7 @@ int main(int argc, const char* argv[]) try { return EXIT_FAILURE; } -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register) try { +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& cmd_register) try { boost::asio::io_context io_ctx; boost::asio::signal_set signals(io_ctx, SIGINT, SIGTERM); diff --git a/src/fusion/CMakeLists.txt b/src/fusion/CMakeLists.txt index b19d84d08..6aea92211 100644 --- a/src/fusion/CMakeLists.txt +++ b/src/fusion/CMakeLists.txt @@ -12,6 +12,8 @@ endif() add_executable(${EXECUTABLE_NAME} main.cpp + CommandHelpers.h + CommandHelpers.cpp ${ICON_RESOURCE} ) diff --git a/src/fusion/CommandHelpers.cpp b/src/fusion/CommandHelpers.cpp new file mode 100644 index 000000000..9946d9b3c --- /dev/null +++ b/src/fusion/CommandHelpers.cpp @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2026 Ember + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "CommandHelpers.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ember::fusion { + +void log_subcommands(log::Logger& logger, const std::string& path, const commands::CommandMap& subcommands) { + LOG_CONSOLE_ASYNC(logger, R"(Available subcomands for "{}": )", path); + + for(auto& subcommand : subcommands | std::views::values) { + LOG_CONSOLE_ASYNC(logger, "{} : {}", subcommand->name(), subcommand->description()); + } +} + +void handle_command_result(commands::Result result, + const std::string& path, + const commands::Command& command, + log::Logger& logger) { + switch(result) { + case commands::Result::missing_args: + [[fallthrough]]; + case commands::Result::too_many_args: + LOG_CONSOLE_ERROR_ASYNC(logger, "Usage: {} {}", path, command.usage_string()); + break; + case commands::Result::subcommands: + log_subcommands(logger, path, command.commands()); + break; + case commands::Result::unavailable: + LOG_CONSOLE_ERROR_ASYNC(logger, R"(Command "{}" is currently unavailable.)", path); + break; + case commands::Result::invalid_types: + LOG_CONSOLE_ERROR_ASYNC(logger, R"(Bad argument types when attempting to execute "{}")", path); + break; + default: + LOG_CONSOLE_ERROR_ASYNC(logger, R"(An unhandled error occurred while executing "{}")", path); + break; + } +} + +void execute_command(const std::string_view input, const Registries& registries, log::Logger& logger) try { + const auto tokens = commands::Registry::parse_input(input); + + // figure out which registry to use + auto prefix = tokens.front(); + const auto it = registries.find(prefix); + + std::span token_view = tokens; + + if(token_view.size() == 1 || !registries.contains(prefix)) { + prefix = "root"; // use the root registry + } else { + token_view = token_view.subspan<1>(); + } + + const auto& registry = registries.at(prefix); + const auto search = registry.find(token_view); + + if(!search.command) { + LOG_CONSOLE_ERROR_ASYNC( + logger, + R"(Command "{}" not found)", + token_view.front() + ); + + return; + } + + auto arguments = token_view.subspan(search.depth); // discard command name token + + /* + * The execution handler validates argument count but the way we iterate for type conversion + * means any unused tokens will be ignored, so the check for too many arguments will never + * trigger from here. This check is only for user feedback, it is not required for correct behaviour. + */ + if(arguments.size() > search.command->argument_count()) { + LOG_CONSOLE_ERROR_ASYNC( + logger, + R"(Too many arguments passed to "{}" (takes {}, got {}))", + token_view.front(), + search.command->argument_count(), + arguments.size() + ); + + return; + } + + // argument type conversion + std::vector arg_values; + auto command_args = search.command->arguments(); + + for(auto [expected, argument] : std::views::zip(command_args, arguments)) { + arg_values.emplace_back(utility::convert_type(expected.type, argument)); + } + + // execute the command handler + if(auto result = search.command->execute(arg_values); result != commands::Result::success) { + const auto& path = commands::path_fragment(token_view, search.depth); + handle_command_result(result, path, *search.command, logger); + } +} catch(const commands::parse_error& e) { + LOG_CONSOLE_ERROR_ASYNC(logger, R"(Error parsing command arguments, "{}")", e.what()); +} catch(const boost::bad_lexical_cast&) { + LOG_CONSOLE_ERROR_ASYNC(logger, R"(Unable to execute command, invalid argument types provided)"); +} catch(const std::exception& e) { + LOG_CONSOLE_ERROR_ASYNC(logger, R"(Error during command execution, "{}")", e.what()); +} + +void handle_help_command(const commands::args::Map& arguments, + const Registries& registries, + log::Logger& logger) { + if(arguments.empty()) { + LOG_CONSOLE_ASYNC( + logger, + R"(Type "help "" (quoted) to display command usage or press tab for autocompletion)" + ); + + return; + } + + const auto command = std::get(arguments.at("command")); + const auto tokens = commands::Registry::parse_input(command); + + // figure out which registry to use + auto prefix = tokens.front(); + const auto it = registries.find(prefix); + + std::span token_view = tokens; + + if(token_view.size() == 1 || !registries.contains(prefix)) { + prefix = "root"; // use the root registry + } else { + token_view = token_view.subspan<1>(); + } + + const auto& registry = registries.at(prefix); + const auto result = registry.find(token_view); + + if(result.command) { + LOG_CONSOLE_ASYNC(logger, "Usage: {} {}", command, result.command->usage_string()); + LOG_CONSOLE_ASYNC(logger, "Description: {}", result.command->description()); + } else { + LOG_CONSOLE_ERROR_ASYNC(logger, R"(Command "{}" not found)", command); + } +} + +#ifdef _WIN32 +void handle_cls_command(log::Logger& logger) { + auto sinks = logger.fetch_sink(log::CommandSink::sink_name); + + if(sinks.empty()) { + LOG_ERROR_SYNC(logger, "Could not locate a command sink, cannot execute command"); + } + + assert(sinks.size() == 1 && "multiple command sinks?"); + + auto command_sink = static_cast(sinks.front().get()); + assert(command_sink->name() == log::CommandSink::sink_name && "unexpected sink name"); + command_sink->clear_console(); +} +#endif + +void register_shared_commands(Registries& registries, log::Logger& logger) { + auto& root = registries["root"]; + + root.insert("help") + ->description("Display console command usage information") + ->optional_argument("command", commands::args::Type::at_string) + ->handler([&](const auto& arguments) { + handle_help_command(arguments, registries, logger); + }); + +#ifdef _WIN32 + root.insert("cls") + ->description("Clears the console") + ->handler([&](const auto&) { + handle_cls_command(logger); + }); +#endif +} + +std::string shortest_common_substring(std::span candidates) { + std::string match; + + if(candidates.empty()) { + return {}; + } else { + match = candidates.front(); + } + + for(const auto& str : candidates) { + std::size_t i = 0; + + while(i < match.size() && i < str.size() && match[i] == str[i]) { + ++i; + } + + match.resize(i); + + if(match.empty()) { + break; + } + } + + return match; +} + +commands::Suggestions handle_autocomplete(Registries& registries, const std::string_view command) { + auto tokens = commands::Registry::parse_input(command); + + // figure out which registry this command could belong to + std::vector> candidates; + + for(const auto& it : registries) { + const auto& [key, value] = it; + + if(tokens.empty()) { + candidates.emplace_back(key, &value); + } else if(key == "root" || key.starts_with(tokens.front())) { + candidates.emplace_back(key, &value); + } + } + + std::vector completions; + std::vector records; + std::string_view split_command; + std::string_view registry_name; + + auto pos = command.find_first_of(' '); + + if(pos == command.npos || pos == command.size() - 1) { + split_command = ""; + } else { + registry_name = registry_name.substr(0, pos); + split_command = command.substr(pos + 1, command.size() - pos); + } + + for(auto& [name, registry] : candidates) { + std::string_view command_view = command; + + if(name != "root") { + command_view = split_command; + } + + // exclude current registry from the search if the prefix doesn't belong to it + if(!registry_name.empty() && registry_name != name) { + continue; + } + + auto suggestions = registry->autocomplete(command_view); + + // populate auto-completion candidate names for these matches + for(const auto& record : suggestions.records) { + if(name != "root") { + completions.emplace_back(std::format("{} {}", name, record.name)); + } else { + completions.emplace_back(record.name); + } + } + + // populate command records, prefixing the current registry name for the table output + for(auto& suggestion : suggestions.records) { + if(name != "root") { + suggestion.name = std::format("{} {}", name, suggestion.name); + } + } + + records.insert_range(records.begin(), suggestions.records); + } + + auto substring = shortest_common_substring(completions); + + return commands::Suggestions { + .substring = std::move(substring), + .records = std::move(records), + }; +} + +void register_command_handlers(Registries& registries, log::Logger& logger) { +#ifdef _WIN32 + auto sinks = logger.fetch_sink(log::CommandSink::sink_name); + + if(sinks.empty()) { + LOG_INFO_SYNC(logger, "Console commands disabled, no suitable logging sink found"); + return; + } else if(sinks.size() > 1) { + LOG_ERROR_SYNC(logger, "Console commands disabled, multiple command logging sinks found"); + return; + } + + auto sink = static_cast(sinks.front().get()); + + sink->register_autocomplete([&](auto cmd) { + return handle_autocomplete(registries, cmd); + }); + + sink->register_handler([&](auto input) { + execute_command(input, registries, logger); + }); +#endif +} + +} // fusion, ember \ No newline at end of file diff --git a/src/fusion/CommandHelpers.h b/src/fusion/CommandHelpers.h new file mode 100644 index 000000000..b4c13b773 --- /dev/null +++ b/src/fusion/CommandHelpers.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Ember + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include +#include + +namespace ember { + +namespace log { class Logger; } + +} // ember + +using Registries = std::unordered_map; + +namespace ember::fusion { + +void register_shared_commands(Registries& registry, log::Logger& logger); +void register_command_handlers(Registries& registry, log::Logger& logger); + +} // fusion, ember \ No newline at end of file diff --git a/src/fusion/main.cpp b/src/fusion/main.cpp index 5d73a2b10..4e42e2df5 100644 --- a/src/fusion/main.cpp +++ b/src/fusion/main.cpp @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "CommandHelpers.h" #include #include #include @@ -14,8 +15,6 @@ #include #include #include -#include -#include #include #include #include @@ -36,10 +35,11 @@ constexpr ember::cstring_view app_name { "Fusion" }; namespace opts = boost::program_options; using namespace ember; +using Registries = std::unordered_map; opts::variables_map parse_arguments(int, const char*[]); opts::variables_map load_options(const std::string&, const opts::options_description&); -int launch(const opts::variables_map&, commands::Registry&, bool, log::Logger&); +int launch(const opts::variables_map&, Registries&, bool, log::Logger&); void launch_dns(const opts::variables_map&, commands::Registry&, bool, log::Logger&); void launch_login(const opts::variables_map&, commands::Registry&, bool, log::Logger&); void launch_gateway(const opts::variables_map&, commands::Registry&, bool, log::Logger&); @@ -51,6 +51,7 @@ void stop_services(); std::vector> stop_handlers; std::atomic_bool shutting_down = false; + int main(int argc, const char* argv[]) try { thread::set_name("Main"); print_banner(app_name); @@ -68,11 +69,11 @@ int main(int argc, const char* argv[]) try { utility::configure_logger(logger, args); log::global_logger(logger); - commands::Registry registry; - utility::register_command_handlers(registry, logger); - utility::register_shared_commands(registry, logger); + Registries registries; + fusion::register_command_handlers(registries, logger); + fusion::register_shared_commands(registries, logger); - const auto ret = launch(args, registry, share_logger, logger); + const auto ret = launch(args, registries, share_logger, logger); LOG_INFO_SYNC(logger, "{} terminated", app_name); return ret; } catch(const std::exception& e) { @@ -80,7 +81,7 @@ int main(int argc, const char* argv[]) try { return EXIT_FAILURE; } -int launch(const opts::variables_map& args, commands::Registry& registry, bool share_logger, log::Logger& logger) try { +int launch(const opts::variables_map& args, Registries& registry, bool share_logger, log::Logger& logger) try { boost::asio::io_context service; boost::asio::signal_set signals(service, SIGINT, SIGTERM); @@ -100,37 +101,37 @@ int launch(const opts::variables_map& args, commands::Registry& registry, bool s if(args["dns.active"].as()) { services.emplace_back(std::jthread([&]() { - launch_dns(args, registry, share_logger, logger); + launch_dns(args, registry["dns"], share_logger, logger); })); } if(args["account.active"].as()) { services.emplace_back(std::jthread([&]() { - launch_account(args, registry, share_logger, logger); + launch_account(args, registry["account"], share_logger, logger); })); } if(args["character.active"].as()) { services.emplace_back(std::jthread([&]() { - launch_character(args, registry, share_logger, logger); + launch_character(args, registry["character"], share_logger, logger); })); } if(args["login.active"].as()) { services.emplace_back(std::jthread([&]() { - launch_login(args, registry, share_logger, logger); + launch_login(args, registry["login"], share_logger, logger); })); } if(args["gateway.active"].as()) { services.emplace_back(std::jthread([&]() { - launch_gateway(args, registry, share_logger, logger); + launch_gateway(args, registry["realm"], share_logger, logger); })); } if(args["world.active"].as()) { services.emplace_back(std::jthread([&]() { - launch_world(args, registry, share_logger, logger); + launch_world(args, registry["world"], share_logger, logger); })); } @@ -181,8 +182,7 @@ void launch_dns(const opts::variables_map& args, commands::Registry& registry, b active_logger = &logger; } - commands::PrefixedRegistry cmd_register(registry, "mdns_"); - dns::Service service(*active_logger, cmd_register); + dns::Service service(*active_logger, registry); stop_handlers.emplace_back([&] { service.stop(); @@ -222,8 +222,7 @@ void launch_login(const opts::variables_map& args, commands::Registry& registry, active_logger = &logger; } - commands::PrefixedRegistry cmd_register(registry, "login_"); - login::Service service(*active_logger, cmd_register); + login::Service service(*active_logger, registry); stop_handlers.emplace_back([&] { service.stop(); @@ -263,8 +262,7 @@ void launch_gateway(const opts::variables_map& args, commands::Registry& registr active_logger = &logger; } - commands::PrefixedRegistry cmd_register(registry, "realm_"); - gateway::Service service(*active_logger, cmd_register); + gateway::Service service(*active_logger, registry); stop_handlers.emplace_back([&] { service.stop(); @@ -304,8 +302,7 @@ void launch_account(const opts::variables_map& args, commands::Registry& registr active_logger = &logger; } - commands::PrefixedRegistry cmd_register(registry, "account_"); - account::Service service(*active_logger, cmd_register); + account::Service service(*active_logger, registry); stop_handlers.emplace_back([&] { service.stop(); @@ -345,8 +342,7 @@ void launch_character(const opts::variables_map& args, commands::Registry& regis active_logger = &logger; } - commands::PrefixedRegistry cmd_register(registry, "character_"); - character::Service service(*active_logger, cmd_register); + character::Service service(*active_logger, registry); stop_handlers.emplace_back([&] { service.stop(); @@ -386,8 +382,7 @@ void launch_world(const opts::variables_map& args, commands::Registry& registry, active_logger = &logger; } - commands::PrefixedRegistry cmd_register(registry, "world_"); - world::Service service(*active_logger, cmd_register); + world::Service service(*active_logger, registry); stop_handlers.emplace_back([&] { service.stop(); diff --git a/src/gateway/Service.h b/src/gateway/Service.h index 78b76a81c..5dfe7e4b7 100644 --- a/src/gateway/Service.h +++ b/src/gateway/Service.h @@ -9,7 +9,7 @@ #pragma once #include -#include +#include #include #include #include @@ -30,7 +30,7 @@ class Service { std::binary_semaphore stop_flag { 0 }; log::Logger& logger; - commands::PrefixedRegistry& cmd_register; + commands::Registry& registry; std::chrono::steady_clock::time_point start_time; void launch(const boost::program_options::variables_map& args, thread::ServicePool& service_pool); @@ -38,10 +38,10 @@ class Service { public: static boost::program_options::options_description options(); - explicit Service(log::Logger& logger, commands::PrefixedRegistry& cmd_register) - : logger(logger), - cmd_register(cmd_register), - start_time(std::chrono::steady_clock::now()) {} + explicit Service(log::Logger& logger, commands::Registry& registry) + : logger(logger) + , registry(registry) + , start_time(std::chrono::steady_clock::now()) {} ~Service() { stop(); diff --git a/src/gateway/main.cpp b/src/gateway/main.cpp index f306f7894..aa5ed7e54 100644 --- a/src/gateway/main.cpp +++ b/src/gateway/main.cpp @@ -9,7 +9,6 @@ #include "Service.h" #include #include -#include #include #include #include @@ -29,7 +28,7 @@ using namespace ember; namespace opts = boost::program_options; opts::variables_map parse_arguments(int argc, const char* argv[]); -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register); +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry); /* * We want to do the minimum amount of work required to get @@ -52,11 +51,10 @@ int main(int argc, const char* argv[]) try { LOG_DEBUG_SYNC(logger, "Registering command handlers..."); commands::Registry registry; - commands::PrefixedRegistry cmd_register(registry); utility::register_command_handlers(registry, logger); utility::register_shared_commands(registry, logger); - const auto ret = run(args, logger, cmd_register); + const auto ret = run(args, logger, registry); LOG_INFO_SYNC(logger, "{} terminated (returned '{}')", gateway::app_name, ret); return ret; } catch(const std::exception& e) { @@ -64,11 +62,11 @@ int main(int argc, const char* argv[]) try { return EXIT_FAILURE; } -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register) try { +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry) try { boost::asio::io_context io_ctx; boost::asio::signal_set signals(io_ctx, SIGINT, SIGTERM); - gateway::Service service(logger, cmd_register); + gateway::Service service(logger, registry); signals.async_wait([&](auto ec, auto signal) { if(ec) { diff --git a/src/libs/core/commands/CMakeLists.txt b/src/libs/core/commands/CMakeLists.txt index 09a667f62..536741fb2 100644 --- a/src/libs/core/commands/CMakeLists.txt +++ b/src/libs/core/commands/CMakeLists.txt @@ -21,7 +21,6 @@ add_library(${LIBRARY_NAME} STATIC include/commands/ArgumentMap.h include/commands/ArgumentType.h include/commands/ArgumentValue.h - include/commands/PrefixedRegistry.h include/commands/ScopedCommand.h include/commands/UserData.h include/commands/Flags.h diff --git a/src/libs/core/commands/include/commands/PrefixedRegistry.h b/src/libs/core/commands/include/commands/PrefixedRegistry.h deleted file mode 100644 index 80c309f6d..000000000 --- a/src/libs/core/commands/include/commands/PrefixedRegistry.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2026 Ember - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#pragma once - -#include -#include -#include - -namespace ember::commands { - -class PrefixedRegistry { - Registry& registry_; - const std::string prefix_; - -public: - PrefixedRegistry(Registry& registry, std::string prefix = {}) - : registry_(registry), - prefix_(std::move(prefix)) {} - - std::shared_ptr operator()(const std::string_view name) { - return registry_.insert(prefix_ + std::string(name)); - } - - ScopedCommand scoped_insert(std::shared_ptr command) { - command->prefix_name(prefix_); - return registry_.scoped_insert(command); - } - - bool erase(const std::shared_ptr command) { - return registry_.erase(command); - } -}; - -} // commands, ember \ No newline at end of file diff --git a/src/libs/mpq/src/MPQ.cpp b/src/libs/mpq/src/MPQ.cpp index db10d257f..1f961fcdc 100644 --- a/src/libs/mpq/src/MPQ.cpp +++ b/src/libs/mpq/src/MPQ.cpp @@ -137,7 +137,7 @@ std::unique_ptr open_archive(const std::filesystem::path& path, stream.close(); - const auto header_v0 = std::start_lifetime_as(h_buf.data()); + const auto header_v0 = std::start_lifetime_as(h_buf.data()); if(!validate_header(*header_v0)) { throw exception("cannot open archive: bad header encountered"); @@ -159,7 +159,7 @@ std::unique_ptr open_archive(const std::filesystem::path& path, std::unique_ptr open_archive(std::span data, const std::uintptr_t offset) { - const auto header_v0 = std::start_lifetime_as(data.data() + offset); + const auto header_v0 = std::start_lifetime_as(data.data() + offset); const auto adjusted = std::span(data.data() + offset, data.size_bytes() - offset); if(header_v0->format_version == 0) { diff --git a/src/libs/mpq/src/v1/MemoryArchive.cpp b/src/libs/mpq/src/v1/MemoryArchive.cpp index d812f3b14..7eca055be 100644 --- a/src/libs/mpq/src/v1/MemoryArchive.cpp +++ b/src/libs/mpq/src/v1/MemoryArchive.cpp @@ -130,7 +130,7 @@ std::span MemoryArchive::fetch_block_table() const { } const Header* MemoryArchive::header() const { - return std::start_lifetime_as(buffer_.data()); + return std::start_lifetime_as
(buffer_.data()); } std::size_t MemoryArchive::size() const { diff --git a/src/libs/shared/shared/utility/CommandHelpers.h b/src/libs/shared/shared/utility/CommandHelpers.h index 3c50bb062..15d48099d 100644 --- a/src/libs/shared/shared/utility/CommandHelpers.h +++ b/src/libs/shared/shared/utility/CommandHelpers.h @@ -8,15 +8,19 @@ #pragma once +#include + namespace ember { -namespace log { class Logger; } -namespace commands { class Registry; class Command; } +namespace log { + class Logger; +} } // ember namespace ember::utility { +commands::args::Value convert_type(commands::args::Type type, std::string_view token); void register_shared_commands(commands::Registry& registry, log::Logger& logger); void register_command_handlers(commands::Registry& registry, log::Logger& logger); diff --git a/src/libs/shared/shared/utility/base32.cpp b/src/libs/shared/shared/utility/base32.cpp index 1938857d5..63ba1d39f 100644 --- a/src/libs/shared/shared/utility/base32.cpp +++ b/src/libs/shared/shared/utility/base32.cpp @@ -19,16 +19,12 @@ #include "base32.h" -int base32_decode(const char* encoded, uint8_t* result, int bufSize) { - return base32_decode(reinterpret_cast(encoded), result, bufSize); -} - -int base32_decode(const uint8_t *encoded, uint8_t *result, int bufSize) { +int base32_decode(const char *encoded, uint8_t *result, int bufSize) { int buffer = 0; int bitsLeft = 0; int count = 0; - for (const uint8_t *ptr = encoded; count < bufSize && *ptr; ++ptr) { - uint8_t ch = *ptr; + for (const char *ptr = encoded; count < bufSize && *ptr; ++ptr) { + char ch = *ptr; if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-') { continue; } diff --git a/src/libs/shared/shared/utility/base32.h b/src/libs/shared/shared/utility/base32.h index 73b5f7b42..2f6c9284f 100644 --- a/src/libs/shared/shared/utility/base32.h +++ b/src/libs/shared/shared/utility/base32.h @@ -31,10 +31,7 @@ #include int base32_decode(const char *encoded, uint8_t *result, int bufSize); -int base32_decode(const uint8_t *encoded, uint8_t *result, int bufSize); - -int base32_encode(const uint8_t *data, int length, uint8_t *result, - int bufSize); +int base32_encode(const uint8_t *data, int length, uint8_t *result, int bufSize); #endif /* _BASE32_H_ */ diff --git a/src/libs/shared/shared/utility/polyfill/start_lifetime_as b/src/libs/shared/shared/utility/polyfill/start_lifetime_as index 4b7d5e429..751ff5a2e 100644 --- a/src/libs/shared/shared/utility/polyfill/start_lifetime_as +++ b/src/libs/shared/shared/utility/polyfill/start_lifetime_as @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Ember + * Copyright (c) 2024 - 2026 Ember * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -15,7 +15,7 @@ // *not possible without compiler machinery** anyway // **the machinery is agreeing to pretend that the compiler might // do something special here but it actually doesn't -#if !defined __cpp_lib_start_lifetime_as +#if !defined __cpp_lib_start_lifetime_as && __GNUC__ < 16 #include diff --git a/src/libs/spark/include/spark/buffers/allocators/BlockAllocator.h b/src/libs/spark/include/spark/buffers/allocators/BlockAllocator.h index 86bec8657..232ad10f9 100644 --- a/src/libs/spark/include/spark/buffers/allocators/BlockAllocator.h +++ b/src/libs/spark/include/spark/buffers/allocators/BlockAllocator.h @@ -9,7 +9,6 @@ #pragma once #include -#include #include #include #include @@ -63,7 +62,7 @@ class BlockAllocator { struct Block { Block() {}; - ~Block() = delete; + ~Block() {}; // this is fine because we're always reading from the member that was last assigned to union { @@ -81,7 +80,7 @@ class BlockAllocator { Block* head_ = nullptr; [[no_unique_address]] tid_type thread_id_; - std::array storage_; + std::array storage_; void page_lock_conditional() { if constexpr(std::is_same_v) { @@ -96,11 +95,8 @@ class BlockAllocator { } void initialise_free_list() { - auto storage = storage_.data(); - - for(std::size_t i = 0; i < _elements; ++i) { - auto block = std::start_lifetime_as(storage + (block_size * i)); - push(block); + for(auto& element : storage_) { + push(&element); } } @@ -170,7 +166,7 @@ class BlockAllocator { inline void deallocate(_ty* t) { assert(t); - auto block = std::start_lifetime_as(t); + auto block = reinterpret_cast(t); if constexpr(std::is_same_v) { assert(block->meta.thread_id == thread_id_ diff --git a/src/login/Service.cpp b/src/login/Service.cpp index 8e00094ad..8805de7b7 100644 --- a/src/login/Service.cpp +++ b/src/login/Service.cpp @@ -323,7 +323,7 @@ void Service::launch(const opts::variables_map& args, boost::asio::io_context& s std::inplace_vector commands; - auto handle = cmd_register.scoped_insert(commands::Command::create("connections") + auto handle = registry.scoped_insert(commands::Command::create("connections") ->description("Display open connection count") ->handler(executor([&](auto& command) { LOG_CONSOLE_ASYNC(logger, "{} active connection(s), {} peak", @@ -333,7 +333,7 @@ void Service::launch(const opts::variables_map& args, boost::asio::io_context& s commands.emplace_back(std::move(handle)); - handle = cmd_register.scoped_insert(commands::Command::create("uptime") + handle = registry.scoped_insert(commands::Command::create("uptime") ->description("Display service uptime") ->handler(executor([&](auto& command) { const auto uptime = std::chrono::steady_clock::now() - start_time; diff --git a/src/login/Service.h b/src/login/Service.h index 5c1355384..26eec6677 100644 --- a/src/login/Service.h +++ b/src/login/Service.h @@ -9,7 +9,7 @@ #pragma once #include -#include +#include #include #include #include @@ -35,7 +35,7 @@ class Service { std::atomic_bool stopping_; log::Logger& logger; - commands::PrefixedRegistry& cmd_register; + commands::Registry& registry; std::chrono::steady_clock::time_point start_time; std::unique_ptr start_metrics( @@ -48,13 +48,13 @@ class Service { public: static boost::program_options::options_description options(); - explicit Service(log::Logger& logger, commands::PrefixedRegistry& cmd_register) - : service(thread::hardware_concurrency()), - serialise(service), - logger(logger), - cmd_register(cmd_register), - start_time(std::chrono::steady_clock::now()), - stopping_(false) {} + explicit Service(log::Logger& logger, commands::Registry& registry) + : service(thread::hardware_concurrency()) + , serialise(service) + , logger(logger) + , registry(registry) + , start_time(std::chrono::steady_clock::now()) + , stopping_(false) {} ~Service() { stop(); diff --git a/src/login/commands/Shutdown.cpp b/src/login/commands/Shutdown.cpp index 7cfbd2219..898488dce 100644 --- a/src/login/commands/Shutdown.cpp +++ b/src/login/commands/Shutdown.cpp @@ -46,13 +46,13 @@ void handle_cancel_command(boost::asio::steady_timer& timer, pending_flag = false; } -void register_shutdown_command(commands::PrefixedRegistry& cmd_register, +void register_shutdown_command(commands::Registry& registry, boost::asio::steady_timer& timer, bool& pending_flag, shutdown::OnInitiate on_initiate, shutdown::OnCancel on_cancel, shutdown::OnExpire on_expire) { - cmd_register("shutdown") + registry.insert("shutdown") ->description("Shuts the service down") ->argument("seconds", commands::args::Type::at_uint32) ->handler([&, on_initiate, on_expire](auto arguments) { diff --git a/src/login/commands/Shutdown.h b/src/login/commands/Shutdown.h index ecf2231c8..490979013 100644 --- a/src/login/commands/Shutdown.h +++ b/src/login/commands/Shutdown.h @@ -8,7 +8,7 @@ #pragma once -#include +#include #include #include #include @@ -23,7 +23,7 @@ using OnExpire = std::function; } // shutdown -void register_shutdown_command(commands::PrefixedRegistry& cmd_register, +void register_shutdown_command(commands::Registry& registry, boost::asio::steady_timer& timer, bool& pending_flag, shutdown::OnInitiate on_initiate, diff --git a/src/login/main.cpp b/src/login/main.cpp index 13dc434a9..ca552b543 100644 --- a/src/login/main.cpp +++ b/src/login/main.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -36,9 +35,9 @@ using namespace ember; namespace opts = boost::program_options; opts::variables_map parse_arguments(int argc, const char* argv[]); -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register); +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry); -void install_shutdown_callbacks(commands::PrefixedRegistry& registry, +void install_shutdown_callbacks(commands::Registry& registry, boost::asio::steady_timer& timer, log::Logger& logger); @@ -63,22 +62,21 @@ int main(int argc, const char* argv[]) try { LOG_DEBUG_SYNC(logger, "Registering command handlers..."); commands::Registry registry; - commands::PrefixedRegistry cmd_register(registry); utility::register_command_handlers(registry, logger); utility::register_shared_commands(registry, logger); - const auto ret = run(args, logger, cmd_register); + const auto ret = run(args, logger, registry); LOG_INFO_SYNC(logger, "{} terminated (returned '{}')", login::app_name, ret); return ret; } catch(const std::exception& e) { std::cerr << e.what(); } -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register) try { +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry) try { boost::asio::io_context io_ctx; boost::asio::signal_set signals(io_ctx, SIGINT, SIGTERM); - login::Service service(logger, cmd_register); + login::Service service(logger, registry); signals.async_wait([&](auto ec, auto signal) { if(ec) { @@ -96,7 +94,7 @@ int run(const opts::variables_map& args, log::Logger& logger, commands::Prefixed }); boost::asio::steady_timer timer(io_ctx); - install_shutdown_callbacks(cmd_register, timer, logger); + install_shutdown_callbacks(registry, timer, logger); const auto ret = service.run(args); signals.cancel(); @@ -106,7 +104,7 @@ int run(const opts::variables_map& args, log::Logger& logger, commands::Prefixed return EXIT_FAILURE; } -void install_shutdown_callbacks(commands::PrefixedRegistry& registry, +void install_shutdown_callbacks(commands::Registry& registry, boost::asio::steady_timer& timer, log::Logger& logger) { static bool pending_flag = false; diff --git a/src/mdns/Service.h b/src/mdns/Service.h index b01fb9e69..97bdc8695 100644 --- a/src/mdns/Service.h +++ b/src/mdns/Service.h @@ -9,7 +9,7 @@ #pragma once #include -#include +#include #include #include #include @@ -27,7 +27,7 @@ class Service { std::binary_semaphore stop_flag { 0 }; log::Logger& logger; - commands::PrefixedRegistry& cmd_register; + commands::Registry& registry; std::chrono::steady_clock::time_point start_time; void launch(const boost::program_options::variables_map& args, boost::asio::io_context& service); @@ -35,10 +35,10 @@ class Service { public: static boost::program_options::options_description options(); - explicit Service(log::Logger& logger, commands::PrefixedRegistry& cmd_register) - : logger(logger), - cmd_register(cmd_register), - start_time(std::chrono::steady_clock::now()) {} + explicit Service(log::Logger& logger, commands::Registry& registry) + : logger(logger) + , registry(registry) + , start_time(std::chrono::steady_clock::now()) {} ~Service() { stop(); diff --git a/src/mdns/main.cpp b/src/mdns/main.cpp index a754289f7..82505df60 100644 --- a/src/mdns/main.cpp +++ b/src/mdns/main.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -27,7 +26,7 @@ using namespace ember; namespace opts = boost::program_options; opts::variables_map parse_arguments(int argc, const char* argv[]); -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register); +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry); /* * We want to do the minimum amount of work required to get @@ -50,11 +49,10 @@ int main(int argc, const char* argv[]) try { LOG_DEBUG_SYNC(logger, "Registering command handlers..."); commands::Registry registry; - commands::PrefixedRegistry cmd_register(registry); utility::register_command_handlers(registry, logger); utility::register_shared_commands(registry, logger); - const auto ret = run(args, logger, cmd_register); + const auto ret = run(args, logger, registry); LOG_INFO_SYNC(logger, "{} terminated (returned '{}')", dns::app_name, ret); return ret; } catch(const std::exception& e) { @@ -62,11 +60,11 @@ int main(int argc, const char* argv[]) try { return EXIT_FAILURE; } -int run(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register) try { +int run(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry) try { boost::asio::io_context io_ctx; boost::asio::signal_set signals(io_ctx, SIGINT, SIGTERM); - dns::Service service(logger, cmd_register); + dns::Service service(logger, registry); signals.async_wait([&](auto ec, auto signal) { if(ec) { diff --git a/src/tools/dbcparser/templates/DiskLoader.cpp_ b/src/tools/dbcparser/templates/DiskLoader.cpp_ index a6b3df592..083aba762 100644 --- a/src/tools/dbcparser/templates/DiskLoader.cpp_ +++ b/src/tools/dbcparser/templates/DiskLoader.cpp_ @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 - 2025 Ember + * Copyright (c) 2014 - 2026 Ember * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -46,8 +46,8 @@ struct MappedDBC { template const MappedDBC get_offsets(const void* start) { const char* data = static_cast(start); - auto dbc = std::start_lifetime_as(data); - auto records = std::start_lifetime_as(data + sizeof(Header)); + auto dbc = std::start_lifetime_as
(data); + auto records = std::start_lifetime_as(data + sizeof(Header)); auto string_block = data + sizeof(Header) + (dbc->record_size * dbc->records); return MappedDBC{dbc, records, string_block}; } diff --git a/src/world/Service.h b/src/world/Service.h index 955722651..ebeda2e85 100644 --- a/src/world/Service.h +++ b/src/world/Service.h @@ -9,7 +9,7 @@ #pragma once #include -#include +#include #include #include #include @@ -21,16 +21,16 @@ constexpr cstring_view app_name { "World Server" }; class Service { log::Logger& logger; - commands::PrefixedRegistry& cmd_register; + commands::Registry& registry; std::chrono::steady_clock::time_point start_time; public: static boost::program_options::options_description options(); - explicit Service(log::Logger& logger, commands::PrefixedRegistry& cmd_register) - : logger(logger), - cmd_register(cmd_register), - start_time(std::chrono::steady_clock::now()) {} + explicit Service(log::Logger& logger, commands::Registry& registry) + : logger(logger) + , registry(registry) + , start_time(std::chrono::steady_clock::now()) {} ~Service() { stop(); diff --git a/src/world/main.cpp b/src/world/main.cpp index 819cf1650..7f6f6053d 100644 --- a/src/world/main.cpp +++ b/src/world/main.cpp @@ -9,7 +9,6 @@ #include "Service.h" #include #include -#include #include #include #include @@ -25,7 +24,7 @@ using namespace ember; namespace opts = boost::program_options; -int launch(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register); +int launch(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry); opts::variables_map parse_arguments(int argc, const char* argv[]); /* @@ -51,11 +50,10 @@ int main(int argc, const char* argv[]) try { LOG_DEBUG_SYNC(logger, "Registering command handlers..."); commands::Registry registry; - commands::PrefixedRegistry cmd_register(registry); utility::register_command_handlers(registry, logger); utility::register_shared_commands(registry, logger); - const auto ret = launch(args, logger, cmd_register); + const auto ret = launch(args, logger, registry); LOG_INFO_SYNC(logger, "{} terminated (returned '{}')", world::app_name, ret); return ret; } catch(const std::exception& e) { @@ -63,8 +61,8 @@ int main(int argc, const char* argv[]) try { return EXIT_FAILURE; } -int launch(const opts::variables_map& args, log::Logger& logger, commands::PrefixedRegistry& cmd_register) try { - world::Service service(logger, cmd_register); +int launch(const opts::variables_map& args, log::Logger& logger, commands::Registry& registry) try { + world::Service service(logger, registry); return service.run(args); } catch(const std::exception& e) { LOG_FATAL_SYNC(logger, "{}", e.what()); diff --git a/tests/Commands.cpp b/tests/Commands.cpp index d21f766ae..abe6efdcf 100644 --- a/tests/Commands.cpp +++ b/tests/Commands.cpp @@ -7,7 +7,6 @@ */ #include -#include #include #include #include @@ -332,53 +331,6 @@ TEST_F(Commands, Execute_AllArgTypes_Success) { ASSERT_TRUE(success); } -TEST_F(Commands, PrefixRegistry_Prefix) { - commands::PrefixedRegistry prefix_reg(registry, "unit_test_"); - auto command = prefix_reg("example"); - ASSERT_EQ(command->name(), "unit_test_example"); -} - -TEST_F(Commands, PrefixRegistry_ScopedPrefix) { - commands::PrefixedRegistry prefix_reg(registry, "unit_test_"); - auto command = commands::Command::create("example"); - auto scoped = prefix_reg.scoped_insert(command); - ASSERT_EQ(command->name(), scoped.command()->name()); - ASSERT_EQ(command->name(), "unit_test_example"); -} - -TEST_F(Commands, PrefixRegistry_Scoping) { - commands::PrefixedRegistry prefix_reg(registry, "unit_test_"); - auto command = commands::Command::create("example"); - - { - auto scoped = prefix_reg.scoped_insert(command); - ASSERT_TRUE(registry.find("unit_test_example").command); - } - - ASSERT_FALSE(registry.find(command->name()).command); -} - -TEST_F(Commands, PrefixRegistry_Release) { - commands::PrefixedRegistry prefix_reg(registry, "unit_test_"); - auto command = commands::Command::create("example"); - { - auto scoped = prefix_reg.scoped_insert(command); - ASSERT_TRUE(registry.find("unit_test_example").command); - scoped.release(); - } - - ASSERT_TRUE(registry.find("unit_test_example").command); -} - -TEST_F(Commands, PrefixRegistry_Reset) { - commands::PrefixedRegistry prefix_reg(registry, "unit_test_"); - auto command = commands::Command::create("example"); - auto scoped = prefix_reg.scoped_insert(command); - ASSERT_TRUE(registry.find("unit_test_example").command); - scoped.reset(); - ASSERT_FALSE(registry.find("unit_test_example").command); -} - TEST_F(Commands, CommandFlags) { auto command = registry.insert("test") ->flags({ 42, 117 });