diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 84a9d84..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ - -language: cpp -os: osx -script: - - g++ -v - - clang++ -v - - cd premake && ./gmake.sh - - cd gmake && make config=release_x32 diff --git a/README.md b/README.md index 0b9aa49..51173c8 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,10 @@ # demboyz we dem boyz -## Build Status -| System | Compiler | Status | -| ------ | -------- | ------ | -| Linux 64-bit | G++-4.8.4 | [![Build Status](https://semaphoreci.com/api/v1/sizzlingcalamari/demboyz/branches/master/badge.svg)](https://semaphoreci.com/sizzlingcalamari/demboyz) | -| Windows | VS2017 | [![Build status](https://ci.appveyor.com/api/projects/status/pc63pbl9b0t5tygl/branch/master?svg=true)](https://ci.appveyor.com/project/SizzlingCalamari/demboyz/branch/master) | -| OSX | Apple LLVM version 7.3.0 (clang-703.0.31) | [![Build Status](https://travis-ci.org/SizzlingStats/demboyz.svg?branch=master)](https://travis-ci.org/SizzlingStats/demboyz) | - -## What is it? - -Demboyz is a command line tool to convert TF2 STV demos into a human readable json format. Demboyz runs on Linux, Windows, and OSX. -It supports the following conversions: - - .dem/.json <-> .dem/.json - .dem/.json -> .con - - The .dem format is the TF2 STV demo format. - The .json format is the demboyz streaming json demo format. - The .con format is a log equal to what would be produced by TF2 - with netmessage/demmessage logs enabled. - -## Potential Uses -* Exporting Stats: Parsing for player stats and motion throughout maps. -* Anticheat: Interpreting player net data for malicious behavior and cheating -* Anti-Anticheat: Modifying player net data to hide your malicious behaviour and cheating -* Censoring: Cover up rude communication by removing player voice and chat messages -* Social engineering: Make your friends seem rude by adding phony player voice and chat messages -* ConeBone69 revival: Change the names of all players to ConeBone69 - -## Usage - - ./demboyz mystvdemo.dem mystvdemo.json - ./demboyz mystvdemo.dem mystvdemo.con - ./demboyz mystvdemo.json mystvdemo.dem - ./demboyz mystvdemo.json mystvdemo.con - -In the first example, mystvdemo.dem will be read from the current working directory, while mystvdemo.json will be written to the current working directory. - -## Contributing - -### For Developers - -If you would like to contribute to demboyz, here are a few tasks up for grabs: - -#### General programming experience: -* Add automated tests to demboyz. - * Using the Catch C++ testing framework. - * Unit test the serialization of network messages. - * System test exact binary matches of dem -> json -> dem transformations. -* Document the .dem format. - * Turn the working demboyz serialization code into a reference manual for the .dem fomat. - * Any amount of work on this helps. Document one struct! -* Develop an app that uses demboyz. - * Provide feedback on the usefulness of the json demo format. - * Create new tools previously impossible to make. - -#### C++ and asm experience: -* Continue reverse engineering the STV demo structs and serialization (svc_ messages). - * Many of the reversed svc_ messages still have unknown binary chunks of data. - * Reverse them for addition to the demboyz source. -* Reverse engineer the POV demo structs and serialization (clc_ messages). - * Currently, demboyz only supports STV demos due to POV demo messages being separate and unknown. - * Work on reverse engineering the clc_ messages just like the svc_ messages. -* Add a conversion to the replay demo format. - * The replay demo format would require reverse engineering just like POV and STV demos. - -### For Users - -* Cheer on the developers. -* Pressure the developers. -* Become a developer. - -## Who Uses demboyz? - -* [KZMod Demo Player [beta]](http://xtreme-jumps.eu/e107_plugins/forum/forum_viewtopic.php?359435) by kraster - -## Compiling From Source - -When following instructions below, the compiled binary will be output in the bin folder. - - -### Using Vagrant - - # Launch VM and build - vagrant up - - # Patch VM and re-build - vagrant provision - - # Manual build - vagrant ssh - cd /vagrant/premake - ./gmake.sh && cd gmake - make - - # Disconnect session and stop VM - exit - vagrant halt - -### With Visual Studio 2013 - - # Generate vs 2013 project - cd premake - vs2013.bat - -Open generated VS solution at premake/vs2013/demboyz.sln +## Build +``` +cd premake/ +premake5 gmake +cd gmake +make -j +``` diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index 67ff6ec..0000000 --- a/Vagrantfile +++ /dev/null @@ -1,125 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - # All Vagrant configuration is done here. The most common configuration - # options are documented and commented below. For a complete reference, - # please see the online documentation at vagrantup.com. - - # Every Vagrant virtual environment requires a box to build off of. - config.vm.box = "ubuntu/trusty64" - - # Disable automatic box update checking. If you disable this, then - # boxes will only be checked for updates when the user runs - # `vagrant box outdated`. This is not recommended. - # config.vm.box_check_update = false - - # Create a forwarded port mapping which allows access to a specific port - # within the machine from a port on the host machine. In the example below, - # accessing "localhost:8080" will access port 80 on the guest machine. - # config.vm.network "forwarded_port", guest: 80, host: 8080 - - # Create a private network, which allows host-only access to the machine - # using a specific IP. - # config.vm.network "private_network", ip: "192.168.33.10" - - # Create a public network, which generally matched to bridged network. - # Bridged networks make the machine appear as another physical device on - # your network. - # config.vm.network "public_network" - - # If true, then any SSH connections made will enable agent forwarding. - # Default value: false - # config.ssh.forward_agent = true - - # Share an additional folder to the guest VM. The first argument is - # the path on the host to the actual folder. The second argument is - # the path on the guest to mount the folder. And the optional third - # argument is a set of non-required options. - # config.vm.synced_folder "../data", "/vagrant_data" - - # Provider-specific configuration so you can fine-tune various - # backing providers for Vagrant. These expose provider-specific options. - # Example for VirtualBox: - # - # config.vm.provider "virtualbox" do |vb| - # # Don't boot with headless mode - # vb.gui = true - # - # # Use VBoxManage to customize the VM. For example to change memory: - # vb.customize ["modifyvm", :id, "--memory", "1024"] - # end - # - # View the documentation for the provider you're using for more - # information on available options. - - # Enable provisioning with CFEngine. CFEngine Community packages are - # automatically installed. For example, configure the host as a - # policy server and optionally a policy file to run: - # - # config.vm.provision "cfengine" do |cf| - # cf.am_policy_hub = true - # # cf.run_file = "motd.cf" - # end - # - # You can also configure and bootstrap a client to an existing - # policy server: - # - # config.vm.provision "cfengine" do |cf| - # cf.policy_server_address = "10.0.2.15" - # end - - # Enable provisioning with Puppet stand alone. Puppet manifests - # are contained in a directory path relative to this Vagrantfile. - # You will need to create the manifests directory and a manifest in - # the file default.pp in the manifests_path directory. - # - # config.vm.provision "puppet" do |puppet| - # puppet.manifests_path = "manifests" - # puppet.manifest_file = "default.pp" - # end - - # Enable provisioning with chef solo, specifying a cookbooks path, roles - # path, and data_bags path (all relative to this Vagrantfile), and adding - # some recipes and/or roles. - # - # config.vm.provision "chef_solo" do |chef| - # chef.cookbooks_path = "../my-recipes/cookbooks" - # chef.roles_path = "../my-recipes/roles" - # chef.data_bags_path = "../my-recipes/data_bags" - # chef.add_recipe "mysql" - # chef.add_role "web" - # - # # You may also specify custom JSON attributes: - # chef.json = { mysql_password: "foo" } - # end - - # Enable provisioning with chef server, specifying the chef server URL, - # and the path to the validation key (relative to this Vagrantfile). - # - # The Opscode Platform uses HTTPS. Substitute your organization for - # ORGNAME in the URL and validation key. - # - # If you have your own Chef Server, use the appropriate URL, which may be - # HTTP instead of HTTPS depending on your configuration. Also change the - # validation key to validation.pem. - # - # config.vm.provision "chef_client" do |chef| - # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" - # chef.validation_key_path = "ORGNAME-validator.pem" - # end - # - # If you're using the Opscode platform, your validator client is - # ORGNAME-validator, replacing ORGNAME with your organization name. - # - # If you have your own Chef Server, the default validation client name is - # chef-validator, unless you changed the configuration. - # - # chef.validation_client_name = "ORGNAME-validator" - - # Shell provisioner - config.vm.provision "shell", path: "vagrant.sh" -end diff --git a/demboyz/base/argparse.hpp b/demboyz/base/argparse.hpp new file mode 100644 index 0000000..d0ce31d --- /dev/null +++ b/demboyz/base/argparse.hpp @@ -0,0 +1,1686 @@ +/* + __ _ _ __ __ _ _ __ __ _ _ __ ___ ___ + / _` | '__/ _` | '_ \ / _` | '__/ __|/ _ \ Argument Parser for Modern C++ +| (_| | | | (_| | |_) | (_| | | \__ \ __/ http://github.com/p-ranav/argparse + \__,_|_| \__, | .__/ \__,_|_| |___/\___| + |___/|_| + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019-2022 Pranav Srinivas Kumar +and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace argparse { + +namespace details { // namespace for helper methods + +template +struct HasContainerTraits : std::false_type {}; + +template <> struct HasContainerTraits : std::false_type {}; + +template <> struct HasContainerTraits : std::false_type {}; + +template +struct HasContainerTraits< + T, std::void_t().begin()), + decltype(std::declval().end()), + decltype(std::declval().size())>> : std::true_type {}; + +template +static constexpr bool IsContainer = HasContainerTraits::value; + +template +struct HasStreamableTraits : std::false_type {}; + +template +struct HasStreamableTraits< + T, + std::void_t() << std::declval())>> + : std::true_type {}; + +template +static constexpr bool IsStreamable = HasStreamableTraits::value; + +constexpr std::size_t repr_max_container_size = 5; + +template std::string repr(T const &val) { + if constexpr (std::is_same_v) { + return val ? "true" : "false"; + } else if constexpr (std::is_convertible_v) { + return '"' + std::string{std::string_view{val}} + '"'; + } else if constexpr (IsContainer) { + std::stringstream out; + out << "{"; + const auto size = val.size(); + if (size > 1) { + out << repr(*val.begin()); + std::for_each( + std::next(val.begin()), + std::next( + val.begin(), + static_cast( + std::min(size, repr_max_container_size) - 1)), + [&out](const auto &v) { out << " " << repr(v); }); + if (size <= repr_max_container_size) { + out << " "; + } else { + out << "..."; + } + } + if (size > 0) { + out << repr(*std::prev(val.end())); + } + out << "}"; + return out.str(); + } else if constexpr (IsStreamable) { + std::stringstream out; + out << val; + return out.str(); + } else { + return ""; + } +} + +namespace { + +template constexpr bool standard_signed_integer = false; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; + +template constexpr bool standard_unsigned_integer = false; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> +constexpr bool standard_unsigned_integer = true; + +} // namespace + +constexpr int radix_8 = 8; +constexpr int radix_10 = 10; +constexpr int radix_16 = 16; + +template +constexpr bool standard_integer = + standard_signed_integer || standard_unsigned_integer; + +template +constexpr decltype(auto) +apply_plus_one_impl(F &&f, Tuple &&t, Extra &&x, + std::index_sequence /*unused*/) { + return std::invoke(std::forward(f), std::get(std::forward(t))..., + std::forward(x)); +} + +template +constexpr decltype(auto) apply_plus_one(F &&f, Tuple &&t, Extra &&x) { + return details::apply_plus_one_impl( + std::forward(f), std::forward(t), std::forward(x), + std::make_index_sequence< + std::tuple_size_v>>{}); +} + +constexpr auto pointer_range(std::string_view s) noexcept { + return std::tuple(s.data(), s.data() + s.size()); +} + +template +constexpr bool starts_with(std::basic_string_view prefix, + std::basic_string_view s) noexcept { + return s.substr(0, prefix.size()) == prefix; +} + +enum class chars_format { + scientific = 0x1, + fixed = 0x2, + hex = 0x4, + general = fixed | scientific +}; + +struct ConsumeHexPrefixResult { + bool is_hexadecimal; + std::string_view rest; +}; + +using namespace std::literals; + +constexpr auto consume_hex_prefix(std::string_view s) + -> ConsumeHexPrefixResult { + if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { + s.remove_prefix(2); + return {true, s}; + } + return {false, s}; +} + +template +inline auto do_from_chars(std::string_view s) -> T { + T x; + auto [first, last] = pointer_range(s); + auto [ptr, ec] = std::from_chars(first, last, x, Param); + if (ec == std::errc()) { + if (ptr == last) { + return x; + } + throw std::invalid_argument{"pattern does not match to the end"}; + } + if (ec == std::errc::invalid_argument) { + throw std::invalid_argument{"pattern not found"}; + } + if (ec == std::errc::result_out_of_range) { + throw std::range_error{"not representable"}; + } + return x; // unreachable +} + +template struct parse_number { + auto operator()(std::string_view s) -> T { + return do_from_chars(s); + } +}; + +template struct parse_number { + auto operator()(std::string_view s) -> T { + if (auto [ok, rest] = consume_hex_prefix(s); ok) { + return do_from_chars(rest); + } + throw std::invalid_argument{"pattern not found"}; + } +}; + +template struct parse_number { + auto operator()(std::string_view s) -> T { + auto [ok, rest] = consume_hex_prefix(s); + if (ok) { + return do_from_chars(rest); + } + if (starts_with("0"sv, s)) { + return do_from_chars(rest); + } + return do_from_chars(rest); + } +}; + +namespace { + +template inline const auto generic_strtod = nullptr; +template <> inline const auto generic_strtod = strtof; +template <> inline const auto generic_strtod = strtod; +template <> inline const auto generic_strtod = strtold; + +} // namespace + +template inline auto do_strtod(std::string const &s) -> T { + if (isspace(static_cast(s[0])) || s[0] == '+') { + throw std::invalid_argument{"pattern not found"}; + } + + auto [first, last] = pointer_range(s); + char *ptr; + + errno = 0; + auto x = generic_strtod(first, &ptr); + if (errno == 0) { + if (ptr == last) { + return x; + } + throw std::invalid_argument{"pattern does not match to the end"}; + } + if (errno == ERANGE) { + throw std::range_error{"not representable"}; + } + return x; // unreachable +} + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { + throw std::invalid_argument{ + "chars_format::general does not parse hexfloat"}; + } + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); !r.is_hexadecimal) { + throw std::invalid_argument{"chars_format::hex parses hexfloat"}; + } + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { + throw std::invalid_argument{ + "chars_format::scientific does not parse hexfloat"}; + } + if (s.find_first_of("eE") == std::string::npos) { + throw std::invalid_argument{ + "chars_format::scientific requires exponent part"}; + } + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { + throw std::invalid_argument{ + "chars_format::fixed does not parse hexfloat"}; + } + if (s.find_first_of("eE") != std::string::npos) { + throw std::invalid_argument{ + "chars_format::fixed does not parse exponent part"}; + } + + return do_strtod(s); + } +}; + +template +std::string join(StrIt first, StrIt last, const std::string &separator) { + if (first == last) { + return ""; + } + std::stringstream value; + value << *first; + ++first; + while (first != last) { + value << separator << *first; + ++first; + } + return value.str(); +} + +} // namespace details + +enum class nargs_pattern { optional, any, at_least_one }; + +enum class default_arguments : unsigned int { + none = 0, + help = 1, + version = 2, + all = help | version, +}; + +inline default_arguments operator&(const default_arguments &a, + const default_arguments &b) { + return static_cast( + static_cast::type>(a) & + static_cast::type>(b)); +} + +class ArgumentParser; + +class Argument { + friend class ArgumentParser; + friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) + -> std::ostream &; + + template + explicit Argument(std::string_view prefix_chars, + std::array &&a, + std::index_sequence /*unused*/) + : m_is_optional((is_optional(a[I], prefix_chars) || ...)), + m_is_required(false), m_is_repeatable(false), m_is_used(false), + m_prefix_chars(prefix_chars) { + ((void)m_names.emplace_back(a[I]), ...); + std::sort( + m_names.begin(), m_names.end(), [](const auto &lhs, const auto &rhs) { + return lhs.size() == rhs.size() ? lhs < rhs : lhs.size() < rhs.size(); + }); + } + +public: + template + explicit Argument(std::string_view prefix_chars, + std::array &&a) + : Argument(prefix_chars, std::move(a), std::make_index_sequence{}) {} + + Argument &help(std::string help_text) { + m_help = std::move(help_text); + return *this; + } + + Argument &metavar(std::string metavar) { + m_metavar = std::move(metavar); + return *this; + } + + template Argument &default_value(T &&value) { + m_default_value_repr = details::repr(value); + m_default_value = std::forward(value); + return *this; + } + + Argument &required() { + m_is_required = true; + return *this; + } + + Argument &implicit_value(std::any value) { + m_implicit_value = std::move(value); + m_num_args_range = NArgsRange{0, 0}; + return *this; + } + + template + auto action(F &&callable, Args &&... bound_args) + -> std::enable_if_t, + Argument &> { + using action_type = std::conditional_t< + std::is_void_v>, + void_action, valued_action>; + if constexpr (sizeof...(Args) == 0) { + m_action.emplace(std::forward(callable)); + } else { + m_action.emplace( + [f = std::forward(callable), + tup = std::make_tuple(std::forward(bound_args)...)]( + std::string const &opt) mutable { + return details::apply_plus_one(f, tup, opt); + }); + } + return *this; + } + + auto &append() { + m_is_repeatable = true; + return *this; + } + + template + auto scan() -> std::enable_if_t, Argument &> { + static_assert(!(std::is_const_v || std::is_volatile_v), + "T should not be cv-qualified"); + auto is_one_of = [](char c, auto... x) constexpr { + return ((c == x) || ...); + }; + + if constexpr (is_one_of(Shape, 'd') && details::standard_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'i') && + details::standard_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'u') && + details::standard_unsigned_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'o') && + details::standard_unsigned_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'x', 'X') && + details::standard_unsigned_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'a', 'A') && + std::is_floating_point_v) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'e', 'E') && + std::is_floating_point_v) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'f', 'F') && + std::is_floating_point_v) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'g', 'G') && + std::is_floating_point_v) { + action(details::parse_number()); + } else { + static_assert(alignof(T) == 0, "No scan specification for T"); + } + + return *this; + } + + Argument &nargs(std::size_t num_args) { + m_num_args_range = NArgsRange{num_args, num_args}; + return *this; + } + + Argument &nargs(std::size_t num_args_min, std::size_t num_args_max) { + m_num_args_range = NArgsRange{num_args_min, num_args_max}; + return *this; + } + + Argument &nargs(nargs_pattern pattern) { + switch (pattern) { + case nargs_pattern::optional: + m_num_args_range = NArgsRange{0, 1}; + break; + case nargs_pattern::any: + m_num_args_range = NArgsRange{0, std::numeric_limits::max()}; + break; + case nargs_pattern::at_least_one: + m_num_args_range = NArgsRange{1, std::numeric_limits::max()}; + break; + } + return *this; + } + + Argument &remaining() { + m_accepts_optional_like_value = true; + return nargs(nargs_pattern::any); + } + + template + Iterator consume(Iterator start, Iterator end, + std::string_view used_name = {}) { + if (!m_is_repeatable && m_is_used) { + throw std::runtime_error("Duplicate argument"); + } + m_is_used = true; + m_used_name = used_name; + + const auto num_args_max = m_num_args_range.get_max(); + const auto num_args_min = m_num_args_range.get_min(); + std::size_t dist = 0; + if (num_args_max == 0) { + m_values.emplace_back(m_implicit_value); + std::visit([](const auto &f) { f({}); }, m_action); + return start; + } + if ((dist = static_cast(std::distance(start, end))) >= + num_args_min) { + if (num_args_max < dist) { + end = std::next(start, static_cast( + num_args_max)); + } + if (!m_accepts_optional_like_value) { + end = std::find_if( + start, end, + std::bind(is_optional, std::placeholders::_1, m_prefix_chars)); + dist = static_cast(std::distance(start, end)); + if (dist < num_args_min) { + throw std::runtime_error("Too few arguments"); + } + } + + struct ActionApply { + void operator()(valued_action &f) { + std::transform(first, last, std::back_inserter(self.m_values), f); + } + + void operator()(void_action &f) { + std::for_each(first, last, f); + if (!self.m_default_value.has_value()) { + if (!self.m_accepts_optional_like_value) { + self.m_values.resize( + static_cast(std::distance(first, last))); + } + } + } + + Iterator first, last; + Argument &self; + }; + std::visit(ActionApply{start, end, *this}, m_action); + return end; + } + if (m_default_value.has_value()) { + return start; + } + throw std::runtime_error("Too few arguments for '" + + std::string(m_used_name) + "'."); + } + + /* + * @throws std::runtime_error if argument values are not valid + */ + void validate() const { + if (m_is_optional) { + // TODO: check if an implicit value was programmed for this argument + if (!m_is_used && !m_default_value.has_value() && m_is_required) { + throw_required_arg_not_used_error(); + } + if (m_is_used && m_is_required && m_values.empty()) { + throw_required_arg_no_value_provided_error(); + } + } else { + if (!m_num_args_range.contains(m_values.size()) && + !m_default_value.has_value()) { + throw_nargs_range_validation_error(); + } + } + } + + std::string get_inline_usage() const { + std::stringstream usage; + // Find the longest variant to show in the usage string + std::string longest_name = m_names.front(); + for (const auto &s : m_names) { + if (s.size() > longest_name.size()) { + longest_name = s; + } + } + if (!m_is_required) { + usage << "["; + } + usage << longest_name; + const std::string metavar = !m_metavar.empty() ? m_metavar : "VAR"; + if (m_num_args_range.get_max() > 0) { + usage << " " << metavar; + if (m_num_args_range.get_max() > 1) { + usage << "..."; + } + } + if (!m_is_required) { + usage << "]"; + } + return usage.str(); + } + + std::size_t get_arguments_length() const { + + std::size_t names_size = std::accumulate( + std::begin(m_names), std::end(m_names), std::size_t(0), + [](const auto &sum, const auto &s) { return sum + s.size(); }); + + if (is_positional(m_names.front(), m_prefix_chars)) { + // A set metavar means this replaces the names + if (!m_metavar.empty()) { + // Indent and metavar + return 2 + m_metavar.size(); + } + + // Indent and space-separated + return 2 + names_size + (m_names.size() - 1); + } + // Is an option - include both names _and_ metavar + // size = text + (", " between names) + std::size_t size = names_size + 2 * (m_names.size() - 1); + if (!m_metavar.empty() && m_num_args_range == NArgsRange{1, 1}) { + size += m_metavar.size() + 1; + } + return size + 2; // indent + } + + friend std::ostream &operator<<(std::ostream &stream, + const Argument &argument) { + std::stringstream name_stream; + name_stream << " "; // indent + if (argument.is_positional(argument.m_names.front(), + argument.m_prefix_chars)) { + if (!argument.m_metavar.empty()) { + name_stream << argument.m_metavar; + } else { + name_stream << details::join(argument.m_names.begin(), + argument.m_names.end(), " "); + } + } else { + name_stream << details::join(argument.m_names.begin(), + argument.m_names.end(), ", "); + // If we have a metavar, and one narg - print the metavar + if (!argument.m_metavar.empty() && + argument.m_num_args_range == NArgsRange{1, 1}) { + name_stream << " " << argument.m_metavar; + } + } + stream << name_stream.str() << "\t" << argument.m_help; + + // print nargs spec + if (!argument.m_help.empty()) { + stream << " "; + } + stream << argument.m_num_args_range; + + if (argument.m_default_value.has_value() && + argument.m_num_args_range != NArgsRange{0, 0}) { + stream << "[default: " << argument.m_default_value_repr << "]"; + } else if (argument.m_is_required) { + stream << "[required]"; + } + stream << "\n"; + return stream; + } + + template bool operator!=(const T &rhs) const { + return !(*this == rhs); + } + + /* + * Compare to an argument value of known type + * @throws std::logic_error in case of incompatible types + */ + template bool operator==(const T &rhs) const { + if constexpr (!details::IsContainer) { + return get() == rhs; + } else { + auto lhs = get(); + return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs), + std::end(rhs), + [](const auto &a, const auto &b) { return a == b; }); + } + } + +private: + class NArgsRange { + std::size_t m_min; + std::size_t m_max; + + public: + NArgsRange(std::size_t minimum, std::size_t maximum) + : m_min(minimum), m_max(maximum) { + if (minimum > maximum) { + throw std::logic_error("Range of number of arguments is invalid"); + } + } + + bool contains(std::size_t value) const { + return value >= m_min && value <= m_max; + } + + bool is_exact() const { return m_min == m_max; } + + bool is_right_bounded() const { + return m_max < std::numeric_limits::max(); + } + + std::size_t get_min() const { return m_min; } + + std::size_t get_max() const { return m_max; } + + // Print help message + friend auto operator<<(std::ostream &stream, const NArgsRange &range) + -> std::ostream & { + if (range.m_min == range.m_max) { + if (range.m_min != 0 && range.m_min != 1) { + stream << "[nargs: " << range.m_min << "] "; + } + } else { + if (range.m_max == std::numeric_limits::max()) { + stream << "[nargs: " << range.m_min << " or more] "; + } else { + stream << "[nargs=" << range.m_min << ".." << range.m_max << "] "; + } + } + return stream; + } + + bool operator==(const NArgsRange &rhs) const { + return rhs.m_min == m_min && rhs.m_max == m_max; + } + + bool operator!=(const NArgsRange &rhs) const { return !(*this == rhs); } + }; + + void throw_nargs_range_validation_error() const { + std::stringstream stream; + if (!m_used_name.empty()) { + stream << m_used_name << ": "; + } else { + stream << m_names.front() << ": "; + } + if (m_num_args_range.is_exact()) { + stream << m_num_args_range.get_min(); + } else if (m_num_args_range.is_right_bounded()) { + stream << m_num_args_range.get_min() << " to " + << m_num_args_range.get_max(); + } else { + stream << m_num_args_range.get_min() << " or more"; + } + stream << " argument(s) expected. " << m_values.size() << " provided."; + throw std::runtime_error(stream.str()); + } + + void throw_required_arg_not_used_error() const { + std::stringstream stream; + stream << m_names.front() << ": required."; + throw std::runtime_error(stream.str()); + } + + void throw_required_arg_no_value_provided_error() const { + std::stringstream stream; + stream << m_used_name << ": no value provided."; + throw std::runtime_error(stream.str()); + } + + static constexpr int eof = std::char_traits::eof(); + + static auto lookahead(std::string_view s) -> int { + if (s.empty()) { + return eof; + } + return static_cast(static_cast(s[0])); + } + + /* + * decimal-literal: + * '0' + * nonzero-digit digit-sequence_opt + * integer-part fractional-part + * fractional-part + * integer-part '.' exponent-part_opt + * integer-part exponent-part + * + * integer-part: + * digit-sequence + * + * fractional-part: + * '.' post-decimal-point + * + * post-decimal-point: + * digit-sequence exponent-part_opt + * + * exponent-part: + * 'e' post-e + * 'E' post-e + * + * post-e: + * sign_opt digit-sequence + * + * sign: one of + * '+' '-' + */ + static bool is_decimal_literal(std::string_view s) { + auto is_digit = [](auto c) constexpr { + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return true; + default: + return false; + } + }; + + // precondition: we have consumed or will consume at least one digit + auto consume_digits = [=](std::string_view sd) { + // NOLINTNEXTLINE(readability-qualified-auto) + auto it = std::find_if_not(std::begin(sd), std::end(sd), is_digit); + return sd.substr(static_cast(it - std::begin(sd))); + }; + + switch (lookahead(s)) { + case '0': { + s.remove_prefix(1); + if (s.empty()) { + return true; + } + goto integer_part; + } + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + s = consume_digits(s); + if (s.empty()) { + return true; + } + goto integer_part_consumed; + } + case '.': { + s.remove_prefix(1); + goto post_decimal_point; + } + default: + return false; + } + + integer_part: + s = consume_digits(s); + integer_part_consumed: + switch (lookahead(s)) { + case '.': { + s.remove_prefix(1); + if (is_digit(lookahead(s))) { + goto post_decimal_point; + } else { + goto exponent_part_opt; + } + } + case 'e': + case 'E': { + s.remove_prefix(1); + goto post_e; + } + default: + return false; + } + + post_decimal_point: + if (is_digit(lookahead(s))) { + s = consume_digits(s); + goto exponent_part_opt; + } + return false; + + exponent_part_opt: + switch (lookahead(s)) { + case eof: + return true; + case 'e': + case 'E': { + s.remove_prefix(1); + goto post_e; + } + default: + return false; + } + + post_e: + switch (lookahead(s)) { + case '-': + case '+': + s.remove_prefix(1); + } + if (is_digit(lookahead(s))) { + s = consume_digits(s); + return s.empty(); + } + return false; + } + + static bool is_optional(std::string_view name, + std::string_view prefix_chars) { + return !is_positional(name, prefix_chars); + } + + /* + * positional: + * _empty_ + * '-' + * '-' decimal-literal + * !'-' anything + */ + static bool is_positional(std::string_view name, + std::string_view prefix_chars) { + auto first = lookahead(name); + + if (first == eof) { + return true; + } else if (prefix_chars.find(static_cast(first)) != + std::string_view::npos) { + name.remove_prefix(1); + if (name.empty()) { + return true; + } + return is_decimal_literal(name); + } + return true; + } + + /* + * Get argument value given a type + * @throws std::logic_error in case of incompatible types + */ + template + auto get() const + -> std::conditional_t, T, const T &> { + if (!m_values.empty()) { + if constexpr (details::IsContainer) { + return any_cast_container(m_values); + } else { + return *std::any_cast(&m_values.front()); + } + } + if (m_default_value.has_value()) { + return *std::any_cast(&m_default_value); + } + if constexpr (details::IsContainer) { + if (!m_accepts_optional_like_value) { + return any_cast_container(m_values); + } + } + + throw std::logic_error("No value provided for '" + m_names.back() + "'."); + } + + /* + * Get argument value given a type. + * @pre The object has no default value. + * @returns The stored value if any, std::nullopt otherwise. + */ + template auto present() const -> std::optional { + if (m_default_value.has_value()) { + throw std::logic_error("Argument with default value always presents"); + } + if (m_values.empty()) { + return std::nullopt; + } + if constexpr (details::IsContainer) { + return any_cast_container(m_values); + } + return std::any_cast(m_values.front()); + } + + template + static auto any_cast_container(const std::vector &operand) -> T { + using ValueType = typename T::value_type; + + T result; + std::transform( + std::begin(operand), std::end(operand), std::back_inserter(result), + [](const auto &value) { return *std::any_cast(&value); }); + return result; + } + + std::vector m_names; + std::string_view m_used_name; + std::string m_help; + std::string m_metavar; + std::any m_default_value; + std::string m_default_value_repr; + std::any m_implicit_value; + using valued_action = std::function; + using void_action = std::function; + std::variant m_action{ + std::in_place_type, + [](const std::string &value) { return value; }}; + std::vector m_values; + NArgsRange m_num_args_range{1, 1}; + bool m_accepts_optional_like_value = false; + bool m_is_optional : true; + bool m_is_required : true; + bool m_is_repeatable : true; + bool m_is_used : true; // True if the optional argument is used by user + std::string_view m_prefix_chars; // ArgumentParser has the prefix_chars +}; + +class ArgumentParser { +public: + explicit ArgumentParser(std::string program_name = {}, + std::string version = "1.0", + default_arguments add_args = default_arguments::all) + : m_program_name(std::move(program_name)), m_version(std::move(version)), + m_parser_path(m_program_name) { + if ((add_args & default_arguments::help) == default_arguments::help) { + add_argument("-h", "--help") + .action([&](const auto & /*unused*/) { + std::cout << help().str(); + std::exit(0); + }) + .default_value(false) + .help("shows help message and exits") + .implicit_value(true) + .nargs(0); + } + if ((add_args & default_arguments::version) == default_arguments::version) { + add_argument("-v", "--version") + .action([&](const auto & /*unused*/) { + std::cout << m_version << std::endl; + std::exit(0); + }) + .default_value(false) + .help("prints version information and exits") + .implicit_value(true) + .nargs(0); + } + } + + ArgumentParser(ArgumentParser &&) noexcept = default; + ArgumentParser &operator=(ArgumentParser &&) = default; + + ArgumentParser(const ArgumentParser &other) + : m_program_name(other.m_program_name), m_version(other.m_version), + m_description(other.m_description), m_epilog(other.m_epilog), + m_prefix_chars(other.m_prefix_chars), + m_assign_chars(other.m_assign_chars), m_is_parsed(other.m_is_parsed), + m_positional_arguments(other.m_positional_arguments), + m_optional_arguments(other.m_optional_arguments), + m_parser_path(other.m_parser_path), m_subparsers(other.m_subparsers) { + for (auto it = std::begin(m_positional_arguments); + it != std::end(m_positional_arguments); ++it) { + index_argument(it); + } + for (auto it = std::begin(m_optional_arguments); + it != std::end(m_optional_arguments); ++it) { + index_argument(it); + } + for (auto it = std::begin(m_subparsers); it != std::end(m_subparsers); + ++it) { + m_subparser_map.insert_or_assign(it->get().m_program_name, it); + m_subparser_used.insert_or_assign(it->get().m_program_name, false); + } + } + + ~ArgumentParser() = default; + + ArgumentParser &operator=(const ArgumentParser &other) { + auto tmp = other; + std::swap(*this, tmp); + return *this; + } + + explicit operator bool() const { + auto arg_used = std::any_of(m_argument_map.cbegin(), + m_argument_map.cend(), + [](auto &it) { + return it.second->m_is_used; + }); + auto subparser_used = std::any_of(m_subparser_used.cbegin(), + m_subparser_used.cend(), + [](auto &it) { + return it.second; + }); + + return m_is_parsed && (arg_used || subparser_used); + } + + // Parameter packing + // Call add_argument with variadic number of string arguments + template Argument &add_argument(Targs... f_args) { + using array_of_sv = std::array; + auto argument = + m_optional_arguments.emplace(std::cend(m_optional_arguments), + m_prefix_chars, array_of_sv{f_args...}); + + if (!argument->m_is_optional) { + m_positional_arguments.splice(std::cend(m_positional_arguments), + m_optional_arguments, argument); + } + + index_argument(argument); + return *argument; + } + + // Parameter packed add_parents method + // Accepts a variadic number of ArgumentParser objects + template + ArgumentParser &add_parents(const Targs &... f_args) { + for (const ArgumentParser &parent_parser : {std::ref(f_args)...}) { + for (const auto &argument : parent_parser.m_positional_arguments) { + auto it = m_positional_arguments.insert( + std::cend(m_positional_arguments), argument); + index_argument(it); + } + for (const auto &argument : parent_parser.m_optional_arguments) { + auto it = m_optional_arguments.insert(std::cend(m_optional_arguments), + argument); + index_argument(it); + } + } + return *this; + } + + ArgumentParser &add_description(std::string description) { + m_description = std::move(description); + return *this; + } + + ArgumentParser &add_epilog(std::string epilog) { + m_epilog = std::move(epilog); + return *this; + } + + /* Getter for arguments and subparsers. + * @throws std::logic_error in case of an invalid argument or subparser name + */ + template + T& at(std::string_view name) { + if constexpr (std::is_same_v) { + return (*this)[name]; + } else { + auto subparser_it = m_subparser_map.find(name); + if (subparser_it != m_subparser_map.end()) { + return subparser_it->second->get(); + } + throw std::logic_error("No such subparser: " + std::string(name)); + } + } + + ArgumentParser &set_prefix_chars(std::string prefix_chars) { + m_prefix_chars = std::move(prefix_chars); + return *this; + } + + ArgumentParser &set_assign_chars(std::string assign_chars) { + m_assign_chars = std::move(assign_chars); + return *this; + } + + /* Call parse_args_internal - which does all the work + * Then, validate the parsed arguments + * This variant is used mainly for testing + * @throws std::runtime_error in case of any invalid argument + */ + void parse_args(const std::vector &arguments) { + parse_args_internal(arguments); + // Check if all arguments are parsed + for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { + argument->validate(); + } + } + + /* Call parse_known_args_internal - which does all the work + * Then, validate the parsed arguments + * This variant is used mainly for testing + * @throws std::runtime_error in case of any invalid argument + */ + std::vector + parse_known_args(const std::vector &arguments) { + auto unknown_arguments = parse_known_args_internal(arguments); + // Check if all arguments are parsed + for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { + argument->validate(); + } + return unknown_arguments; + } + + /* Main entry point for parsing command-line arguments using this + * ArgumentParser + * @throws std::runtime_error in case of any invalid argument + */ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) + void parse_args(int argc, const char *const argv[]) { + parse_args({argv, argv + argc}); + } + + /* Main entry point for parsing command-line arguments using this + * ArgumentParser + * @throws std::runtime_error in case of any invalid argument + */ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) + auto parse_known_args(int argc, const char *const argv[]) { + return parse_known_args({argv, argv + argc}); + } + + /* Getter for options with default values. + * @throws std::logic_error if parse_args() has not been previously called + * @throws std::logic_error if there is no such option + * @throws std::logic_error if the option has no value + * @throws std::bad_any_cast if the option is not of type T + */ + template + auto get(std::string_view arg_name) const + -> std::conditional_t, T, const T &> { + if (!m_is_parsed) { + throw std::logic_error("Nothing parsed, no arguments are available."); + } + return (*this)[arg_name].get(); + } + + /* Getter for options without default values. + * @pre The option has no default value. + * @throws std::logic_error if there is no such option + * @throws std::bad_any_cast if the option is not of type T + */ + template + auto present(std::string_view arg_name) const -> std::optional { + return (*this)[arg_name].present(); + } + + /* Getter that returns true for user-supplied options. Returns false if not + * user-supplied, even with a default value. + */ + auto is_used(std::string_view arg_name) const { + return (*this)[arg_name].m_is_used; + } + + /* Getter that returns true if a subcommand is used. + */ + auto is_subcommand_used(std::string_view subcommand_name) const { + return m_subparser_used.at(subcommand_name); + } + + /* Getter that returns true if a subcommand is used. + */ + auto is_subcommand_used(const ArgumentParser &subparser) const { + return is_subcommand_used(subparser.m_program_name); + } + + /* Indexing operator. Return a reference to an Argument object + * Used in conjuction with Argument.operator== e.g., parser["foo"] == true + * @throws std::logic_error in case of an invalid argument name + */ + Argument &operator[](std::string_view arg_name) const { + auto it = m_argument_map.find(arg_name); + if (it != m_argument_map.end()) { + return *(it->second); + } + if (!is_valid_prefix_char(arg_name.front())) { + std::string name(arg_name); + const auto legal_prefix_char = get_any_valid_prefix_char(); + const auto prefix = std::string(1, legal_prefix_char); + + // "-" + arg_name + name = prefix + name; + it = m_argument_map.find(name); + if (it != m_argument_map.end()) { + return *(it->second); + } + // "--" + arg_name + name = prefix + name; + it = m_argument_map.find(name); + if (it != m_argument_map.end()) { + return *(it->second); + } + } + throw std::logic_error("No such argument: " + std::string(arg_name)); + } + + // Print help message + friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) + -> std::ostream & { + stream.setf(std::ios_base::left); + + auto longest_arg_length = parser.get_length_of_longest_argument(); + + stream << parser.usage() << "\n\n"; + + if (!parser.m_description.empty()) { + stream << parser.m_description << "\n\n"; + } + + if (!parser.m_positional_arguments.empty()) { + stream << "Positional arguments:\n"; + } + + for (const auto &argument : parser.m_positional_arguments) { + stream.width(static_cast(longest_arg_length)); + stream << argument; + } + + if (!parser.m_optional_arguments.empty()) { + stream << (parser.m_positional_arguments.empty() ? "" : "\n") + << "Optional arguments:\n"; + } + + for (const auto &argument : parser.m_optional_arguments) { + stream.width(static_cast(longest_arg_length)); + stream << argument; + } + + if (!parser.m_subparser_map.empty()) { + stream << (parser.m_positional_arguments.empty() + ? (parser.m_optional_arguments.empty() ? "" : "\n") + : "\n") + << "Subcommands:\n"; + for (const auto &[command, subparser] : parser.m_subparser_map) { + stream << std::setw(2) << " "; + stream << std::setw(static_cast(longest_arg_length - 2)) + << command; + stream << " " << subparser->get().m_description << "\n"; + } + } + + if (!parser.m_epilog.empty()) { + stream << '\n'; + stream << parser.m_epilog << "\n\n"; + } + + return stream; + } + + // Format help message + auto help() const -> std::stringstream { + std::stringstream out; + out << *this; + return out; + } + + // Format usage part of help only + auto usage() const -> std::string { + std::stringstream stream; + + stream << "Usage: " << this->m_program_name; + + // Add any options inline here + for (const auto &argument : this->m_optional_arguments) { + stream << " " << argument.get_inline_usage(); + } + // Put positional arguments after the optionals + for (const auto &argument : this->m_positional_arguments) { + if (!argument.m_metavar.empty()) { + stream << " " << argument.m_metavar; + } else { + stream << " " << argument.m_names.front(); + } + } + // Put subcommands after positional arguments + if (!m_subparser_map.empty()) { + stream << " {"; + std::size_t i{0}; + for (const auto &[command, unused] : m_subparser_map) { + if (i == 0) { + stream << command; + } else { + stream << "," << command; + } + ++i; + } + stream << "}"; + } + + return stream.str(); + } + + // Printing the one and only help message + // I've stuck with a simple message format, nothing fancy. + [[deprecated("Use cout << program; instead. See also help().")]] std::string + print_help() const { + auto out = help(); + std::cout << out.rdbuf(); + return out.str(); + } + + void add_subparser(ArgumentParser &parser) { + parser.m_parser_path = m_program_name + " " + parser.m_program_name; + auto it = m_subparsers.emplace(std::cend(m_subparsers), parser); + m_subparser_map.insert_or_assign(parser.m_program_name, it); + m_subparser_used.insert_or_assign(parser.m_program_name, false); + } + +private: + bool is_valid_prefix_char(char c) const { + return m_prefix_chars.find(c) != std::string::npos; + } + + char get_any_valid_prefix_char() const { return m_prefix_chars[0]; } + + /* + * Pre-process this argument list. Anything starting with "--", that + * contains an =, where the prefix before the = has an entry in the + * options table, should be split. + */ + std::vector + preprocess_arguments(const std::vector &raw_arguments) const { + std::vector arguments{}; + for (const auto &arg : raw_arguments) { + + const auto argument_starts_with_prefix_chars = + [this](const std::string &a) -> bool { + if (!a.empty()) { + + const auto legal_prefix = [this](char c) -> bool { + return m_prefix_chars.find(c) != std::string::npos; + }; + + // Windows-style + // if '/' is a legal prefix char + // then allow single '/' followed by argument name, followed by an + // assign char, e.g., ':' e.g., 'test.exe /A:Foo' + const auto windows_style = legal_prefix('/'); + + if (windows_style) { + if (legal_prefix(a[0])) { + return true; + } + } else { + // Slash '/' is not a legal prefix char + // For all other characters, only support long arguments + // i.e., the argument must start with 2 prefix chars, e.g, + // '--foo' e,g, './test --foo=Bar -DARG=yes' + if (a.size() > 1) { + return (legal_prefix(a[0]) && legal_prefix(a[1])); + } + } + } + return false; + }; + + // Check that: + // - We don't have an argument named exactly this + // - The argument starts with a prefix char, e.g., "--" + // - The argument contains an assign char, e.g., "=" + auto assign_char_pos = arg.find_first_of(m_assign_chars); + + if (m_argument_map.find(arg) == m_argument_map.end() && + argument_starts_with_prefix_chars(arg) && + assign_char_pos != std::string::npos) { + // Get the name of the potential option, and check it exists + std::string opt_name = arg.substr(0, assign_char_pos); + if (m_argument_map.find(opt_name) != m_argument_map.end()) { + // This is the name of an option! Split it into two parts + arguments.push_back(std::move(opt_name)); + arguments.push_back(arg.substr(assign_char_pos + 1)); + continue; + } + } + // If we've fallen through to here, then it's a standard argument + arguments.push_back(arg); + } + return arguments; + } + + /* + * @throws std::runtime_error in case of any invalid argument + */ + void parse_args_internal(const std::vector &raw_arguments) { + auto arguments = preprocess_arguments(raw_arguments); + if (m_program_name.empty() && !arguments.empty()) { + m_program_name = arguments.front(); + } + auto end = std::end(arguments); + auto positional_argument_it = std::begin(m_positional_arguments); + for (auto it = std::next(std::begin(arguments)); it != end;) { + const auto ¤t_argument = *it; + if (Argument::is_positional(current_argument, m_prefix_chars)) { + if (positional_argument_it == std::end(m_positional_arguments)) { + + std::string_view maybe_command = current_argument; + + // Check sub-parsers + auto subparser_it = m_subparser_map.find(maybe_command); + if (subparser_it != m_subparser_map.end()) { + + // build list of remaining args + const auto unprocessed_arguments = + std::vector(it, end); + + // invoke subparser + m_is_parsed = true; + m_subparser_used[maybe_command] = true; + return subparser_it->second->get().parse_args( + unprocessed_arguments); + } + + throw std::runtime_error( + "Maximum number of positional arguments exceeded"); + } + auto argument = positional_argument_it++; + it = argument->consume(it, end); + continue; + } + + auto arg_map_it = m_argument_map.find(current_argument); + if (arg_map_it != m_argument_map.end()) { + auto argument = arg_map_it->second; + it = argument->consume(std::next(it), end, arg_map_it->first); + } else if (const auto &compound_arg = current_argument; + compound_arg.size() > 1 && + is_valid_prefix_char(compound_arg[0]) && + !is_valid_prefix_char(compound_arg[1])) { + ++it; + for (std::size_t j = 1; j < compound_arg.size(); j++) { + auto hypothetical_arg = std::string{'-', compound_arg[j]}; + auto arg_map_it2 = m_argument_map.find(hypothetical_arg); + if (arg_map_it2 != m_argument_map.end()) { + auto argument = arg_map_it2->second; + it = argument->consume(it, end, arg_map_it2->first); + } else { + throw std::runtime_error("Unknown argument: " + current_argument); + } + } + } else { + throw std::runtime_error("Unknown argument: " + current_argument); + } + } + m_is_parsed = true; + } + + /* + * Like parse_args_internal but collects unused args into a vector + */ + std::vector + parse_known_args_internal(const std::vector &raw_arguments) { + auto arguments = preprocess_arguments(raw_arguments); + + std::vector unknown_arguments{}; + + if (m_program_name.empty() && !arguments.empty()) { + m_program_name = arguments.front(); + } + auto end = std::end(arguments); + auto positional_argument_it = std::begin(m_positional_arguments); + for (auto it = std::next(std::begin(arguments)); it != end;) { + const auto ¤t_argument = *it; + if (Argument::is_positional(current_argument, m_prefix_chars)) { + if (positional_argument_it == std::end(m_positional_arguments)) { + + std::string_view maybe_command = current_argument; + + // Check sub-parsers + auto subparser_it = m_subparser_map.find(maybe_command); + if (subparser_it != m_subparser_map.end()) { + + // build list of remaining args + const auto unprocessed_arguments = + std::vector(it, end); + + // invoke subparser + m_is_parsed = true; + m_subparser_used[maybe_command] = true; + return subparser_it->second->get().parse_known_args_internal( + unprocessed_arguments); + } + + // save current argument as unknown and go to next argument + unknown_arguments.push_back(current_argument); + ++it; + } else { + // current argument is the value of a positional argument + // consume it + auto argument = positional_argument_it++; + it = argument->consume(it, end); + } + continue; + } + + auto arg_map_it = m_argument_map.find(current_argument); + if (arg_map_it != m_argument_map.end()) { + auto argument = arg_map_it->second; + it = argument->consume(std::next(it), end, arg_map_it->first); + } else if (const auto &compound_arg = current_argument; + compound_arg.size() > 1 && + is_valid_prefix_char(compound_arg[0]) && + !is_valid_prefix_char(compound_arg[1])) { + ++it; + for (std::size_t j = 1; j < compound_arg.size(); j++) { + auto hypothetical_arg = std::string{'-', compound_arg[j]}; + auto arg_map_it2 = m_argument_map.find(hypothetical_arg); + if (arg_map_it2 != m_argument_map.end()) { + auto argument = arg_map_it2->second; + it = argument->consume(it, end, arg_map_it2->first); + } else { + unknown_arguments.push_back(current_argument); + break; + } + } + } else { + // current argument is an optional-like argument that is unknown + // save it and move to next argument + unknown_arguments.push_back(current_argument); + ++it; + } + } + m_is_parsed = true; + return unknown_arguments; + } + + // Used by print_help. + std::size_t get_length_of_longest_argument() const { + if (m_argument_map.empty()) { + return 0; + } + std::size_t max_size = 0; + for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { + max_size = std::max(max_size, argument->get_arguments_length()); + } + for ([[maybe_unused]] const auto &[command, unused] : m_subparser_map) { + max_size = std::max(max_size, command.size()); + } + return max_size; + } + + using argument_it = std::list::iterator; + using argument_parser_it = + std::list>::iterator; + + void index_argument(argument_it it) { + for (const auto &name : std::as_const(it->m_names)) { + m_argument_map.insert_or_assign(name, it); + } + } + + std::string m_program_name; + std::string m_version; + std::string m_description; + std::string m_epilog; + std::string m_prefix_chars{"-"}; + std::string m_assign_chars{"="}; + bool m_is_parsed = false; + std::list m_positional_arguments; + std::list m_optional_arguments; + std::map m_argument_map; + std::string m_parser_path; + std::list> m_subparsers; + std::map m_subparser_map; + std::map m_subparser_used; +}; + +} // namespace argparse diff --git a/demboyz/demboyz.cpp b/demboyz/demboyz.cpp index 71de343..59ccb78 100644 --- a/demboyz/demboyz.cpp +++ b/demboyz/demboyz.cpp @@ -1,6 +1,7 @@ #include "io/demoreader.h" #include "game/sourcecontext.h" +#include #include #include #include @@ -8,34 +9,57 @@ int main(const int argc, const char* argv[]) { - if (argc < 2) - { - fprintf(stderr, "Usage: %s .dem [in2.dem] ...\n", argv[0]); - return -1; - } - DemoReader::Init(); SourceGameContext *context = nullptr; + argparse::ArgumentParser program("demboyz", "1.0", argparse::default_arguments::help); + program.add_argument("-l", "--long") + .default_value(false) + .implicit_value(true) + .help("Don't skip silence."); + program.add_argument("files") + .help("Demo files [.dem]") + .nargs(argparse::nargs_pattern::at_least_one); + program.add_description("Multiple demo files are only supported for split demos, supply in correct order."); + + try { + program.parse_args(argc, argv); + } + catch (const std::runtime_error& err) { + std::cerr << err.what() << std::endl; + std::cerr << program; + return -1; + } + + bool bSkipSilence = !program.get("--long"); + std::vector files; + try { + files = program.get>("files"); + } catch (std::logic_error& e) { + std::cerr << "ERROR: No demo files provided!" << std::endl << std::endl; + std::cerr << program; + return -1; + } + bool error = false; - for (int i = 1; i < argc; i++) + for (unsigned int i = 0; i < files.size(); i++) { - std::filesystem::path inputFile(argv[i]); - FILE* inputFp = fopen(inputFile.c_str(), "rb"); + std::filesystem::path inputFile(files[i]); + FILE* inputFp = fopen((char *)inputFile.c_str(), "rb"); if (!inputFp) { - fprintf(stderr, "Error: Could not open input file\n"); + fprintf(stderr, "ERROR: Could not open input file '%s'\n", inputFile.c_str()); return -1; } static std::filesystem::path outputDir = inputFile.filename().replace_extension(); static std::filesystem::path outputDirVoice = outputDir.string() + "/voice"; - if (i == 1) + if (i == 0) { std::filesystem::create_directory(outputDir); std::filesystem::create_directory(outputDirVoice); - context = new SourceGameContext(outputDir, outputDirVoice); + context = new SourceGameContext(outputDir.string(), outputDirVoice.string(), bSkipSilence); if(!context->init()) return -1; } diff --git a/demboyz/game/logic.cpp b/demboyz/game/logic.cpp index c49e32f..500c605 100644 --- a/demboyz/game/logic.cpp +++ b/demboyz/game/logic.cpp @@ -78,6 +78,7 @@ void Logic::End() data["voice"]["total_time"] = voiceTotalTime; data["voice"]["active_time"] = voiceActiveTime; + data["voice"]["silence"] = json::array(); for(const auto& o : context->voiceWriter->m_silence) { data["voice"]["silence"] += json({o.first, o.second}); diff --git a/demboyz/game/sourcecontext.cpp b/demboyz/game/sourcecontext.cpp index 2eb372b..f298aff 100644 --- a/demboyz/game/sourcecontext.cpp +++ b/demboyz/game/sourcecontext.cpp @@ -15,9 +15,10 @@ #include "netmessages/svc_voiceinit.h" #include "netmessages/svc_voicedata.h" -SourceGameContext::SourceGameContext(std::string outputDir, std::string outputDirVoice): +SourceGameContext::SourceGameContext(std::string outputDir, std::string outputDirVoice, bool bSkipSilence): outputDir(outputDir), - outputDirVoice(outputDirVoice) + outputDirVoice(outputDirVoice), + m_bSkipSilence(bSkipSilence) { stringTables = new StringTableContainer(this); userIdLookUp = new uint8_t[USHRT_MAX+1]; @@ -51,7 +52,7 @@ bool SourceGameContext::init() return false; } - voiceWriter = new VoiceDataWriter(this, outputDirVoice.c_str()); + voiceWriter = new VoiceDataWriter(this, outputDirVoice.c_str(), m_bSkipSilence); if(!voiceWriter->init()) return false; diff --git a/demboyz/game/sourcecontext.h b/demboyz/game/sourcecontext.h index 7db7a09..0247922 100644 --- a/demboyz/game/sourcecontext.h +++ b/demboyz/game/sourcecontext.h @@ -69,7 +69,7 @@ typedef struct player_info_s struct SourceGameContext { - SourceGameContext(std::string outputDir, std::string outputDirVoice); + SourceGameContext(std::string outputDir, std::string outputDirVoice, bool bSkipSilence); ~SourceGameContext(); bool init(); @@ -88,6 +88,7 @@ struct SourceGameContext std::string outputDir; std::string outputDirVoice; + bool m_bSkipSilence; FILE* outputFp; Logic* logic; diff --git a/demboyz/io/voicewriter/voicedatawriter.cpp b/demboyz/io/voicewriter/voicedatawriter.cpp index b425323..a332cab 100644 --- a/demboyz/io/voicewriter/voicedatawriter.cpp +++ b/demboyz/io/voicewriter/voicedatawriter.cpp @@ -132,9 +132,10 @@ int SilkVoiceDecoder::Decompress( } -VoiceDataWriter::VoiceDataWriter(SourceGameContext* context, const char* outputPath): +VoiceDataWriter::VoiceDataWriter(SourceGameContext* context, const char* outputPath, bool bSkipSilence): context(context), - m_outputPath(outputPath) + m_outputPath(outputPath), + m_bSkipSilence(bSkipSilence) { } @@ -195,7 +196,7 @@ void VoiceDataWriter::EndCommandPacket(const PacketTrailingBits& trailingBits) return; // Skip silence if noone talks for at least 1.5 seconds - if((m_curTick - m_lastVoiceTick) / context->fTickRate > 1.5) + if((m_curTick - m_lastVoiceTick) / context->fTickRate > 1.5 && m_bSkipSilence) { if(!m_isSilenced) { diff --git a/demboyz/io/voicewriter/voicedatawriter.h b/demboyz/io/voicewriter/voicedatawriter.h index 77904c7..8e0696a 100644 --- a/demboyz/io/voicewriter/voicedatawriter.h +++ b/demboyz/io/voicewriter/voicedatawriter.h @@ -53,7 +53,7 @@ private: class VoiceDataWriter { public: - VoiceDataWriter(SourceGameContext *context, const char* outputPath); + VoiceDataWriter(SourceGameContext *context, const char* outputPath, bool bSkipSilence); bool init(); void Start(); @@ -90,6 +90,7 @@ private: int32_t m_silenceTicks = 0; int32_t m_silenceTicksStart = 0; const char* m_outputPath = nullptr; + bool m_bSkipSilence = true; int16_t m_decodeBuffer[32768]; diff --git a/premake/gmake.sh b/premake/gmake.sh deleted file mode 100755 index 40060c3..0000000 --- a/premake/gmake.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -if [ "$(uname)" == "Darwin" ]; then - # Do something under Mac OS X platform - ./premake5_osx gmake --file=premake5.lua -elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then - # Do something under Linux platform - ./premake5_linux gmake --file=premake5.lua -elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then - # Do something under Windows NT platform - echo "windows? don't use this" -fi - diff --git a/premake/premake5.exe b/premake/premake5.exe deleted file mode 100644 index ebe7ae7..0000000 Binary files a/premake/premake5.exe and /dev/null differ diff --git a/premake/premake5.lua b/premake/premake5.lua index 22cf36a..b2fc95e 100644 --- a/premake/premake5.lua +++ b/premake/premake5.lua @@ -1,25 +1,26 @@ - solution "demboyz" basedir ".." location (_ACTION) targetdir "../bin" startproject "demboyz" configurations { "Release", "Debug" } - flags { "MultiProcessorCompile", "Symbols" } + flags { "MultiProcessorCompile" } + symbols "On" - configuration "Debug" + filter "configurations:Debug" defines { "DEBUG" } - configuration "Release" + filter "configurations:Release" optimize "Full" - configuration {} + filter {} project "demboyz" kind "ConsoleApp" language "C++" - configuration "gmake" + filter "configurations:gmake" buildoptions { "-std=c++17 -Wall" } linkoptions { "-flto -no-pie -Wall" } - configuration {} + filter {} + files { "../demboyz/**.h", diff --git a/premake/premake5_linux b/premake/premake5_linux deleted file mode 100755 index 7dceda1..0000000 Binary files a/premake/premake5_linux and /dev/null differ diff --git a/premake/premake5_osx b/premake/premake5_osx deleted file mode 100755 index 863d144..0000000 Binary files a/premake/premake5_osx and /dev/null differ diff --git a/premake/sourcesdk.lua b/premake/sourcesdk.lua index d69482f..b09cdeb 100644 --- a/premake/sourcesdk.lua +++ b/premake/sourcesdk.lua @@ -7,9 +7,10 @@ group "external" kind "StaticLib" language "C++" location (_ACTION .. "/" .. project().name) - configuration "gmake" + filter "configurations:gmake" buildoptions { "-std=c++11" } - configuration {} + filter {} + includedirs { base_dir .. "include/", diff --git a/vagrant.sh b/vagrant.sh deleted file mode 100755 index 49f9372..0000000 --- a/vagrant.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -# Install g++ dependencies -sudo apt-get install -y build-essential g++-4.8 g++-4.8-multilib