diff --git a/extension/doc_classes/ArgumentOption.xml b/extension/doc_classes/ArgumentOption.xml
index be80b44c..ac15c3cd 100644
--- a/extension/doc_classes/ArgumentOption.xml
+++ b/extension/doc_classes/ArgumentOption.xml
@@ -28,6 +28,8 @@
The alternative names the option can be known by.
+
+
The default value the option returns if not set.
@@ -46,4 +48,14 @@
The expected type of option.
+
+
+
+
+
+
+
+
+
+
diff --git a/extension/doc_classes/ArgumentParser.xml b/extension/doc_classes/ArgumentParser.xml
index 5ff5e793..b0fb0078 100644
--- a/extension/doc_classes/ArgumentParser.xml
+++ b/extension/doc_classes/ArgumentParser.xml
@@ -47,6 +47,7 @@
+
Parses the arguments for the commandline into the [ArgumentParser]. This is run before game initialization for [method OS.get_cmdline_args] and [method OS.get_cmdline_user_args].
Prints errors for unknown arguments when [param error_unknown] is true.
diff --git a/extension/src/openvic-extension/core/ArgumentParser.cpp b/extension/src/openvic-extension/core/ArgumentParser.cpp
index d7e2a628..7242dba9 100644
--- a/extension/src/openvic-extension/core/ArgumentParser.cpp
+++ b/extension/src/openvic-extension/core/ArgumentParser.cpp
@@ -4,11 +4,16 @@
#include
#include
#include
+#include
#include
#include
#include
#include
#include
+#include
+#include
+#include
+#include
#include
#include
#include
@@ -53,10 +58,10 @@ using namespace godot;
ArgumentOption::ArgumentOption(
StringName const& name, Variant const& default_value, String const& description, PackedStringArray const& aliases,
- String option_args
+ String const& option_args, BitField conflict_type
)
: name { name }, default_value { default_value }, description { description }, aliases(aliases),
- option_arguments { option_args } {
+ option_arguments { option_args }, conflict_type { conflict_type } {
aliases_set.reserve(aliases.size());
for (String const& alias : aliases) {
aliases_set.insert(alias);
@@ -91,6 +96,10 @@ String ArgumentOption::get_description() const {
return description;
}
+BitField ArgumentOption::get_conflict_type() const {
+ return conflict_type;
+}
+
String ArgumentOption::get_help_string(bool p_is_rich) const {
static constexpr int64_t NAME_PADDING = 45;
static constexpr int64_t TYPE_PADDING = 45;
@@ -187,16 +196,16 @@ Variant ArgumentOption::_get_empty_value_for(Variant::Type p_type) {
Ref ArgumentOption::create(
StringName const& p_name, Variant::Type p_type, String const& p_description, PackedStringArray const& p_aliases,
- String p_option_args
+ String const& p_option_args, BitField p_conflict_type
) {
- return create_with_default(p_name, _get_empty_value_for(p_type), p_description, p_aliases, p_option_args);
+ return create_with_default(p_name, _get_empty_value_for(p_type), p_description, p_aliases, p_option_args, p_conflict_type);
}
Ref ArgumentOption::create_with_default(
StringName const& p_name, Variant const& p_default, String const& p_description, PackedStringArray const& p_aliases,
- String p_option_args
+ String const& p_option_args, BitField p_conflict_type
) {
- return memnew(ArgumentOption(p_name, p_default, p_description, p_aliases, p_option_args));
+ return memnew(ArgumentOption(p_name, p_default, p_description, p_aliases, p_option_args, p_conflict_type));
}
void ArgumentOption::_bind_methods() {
@@ -230,6 +239,14 @@ void ArgumentOption::_bind_methods() {
OV_BIND_METHOD(ArgumentOption::get_description);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "description"), "", "get_description");
+ BIND_BITFIELD_FLAG(ARGUMENT_CONFLICT_NONE);
+ BIND_BITFIELD_FLAG(ARGUMENT_CONFLICT_SHORTHAND);
+ BIND_BITFIELD_FLAG(ARGUMENT_CONFLICT_LONG);
+ BIND_BITFIELD_FLAG(ARGUMENT_CONFLICT_ALIASES);
+
+ OV_BIND_METHOD(ArgumentOption::get_conflict_type);
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "conflict_type"), "", "get_conflict_type");
+
OV_BIND_METHOD(ArgumentOption::get_help_string, { "is_rich" });
}
@@ -237,18 +254,30 @@ ArgumentParser::ArgumentParser() {
singleton = this;
options.push_back(ArgumentOption::create("help", Variant::BOOL, "Displays help and quits.", { "h" }));
+
+ options.push_back(
+ ArgumentOption::create(
+ "game-debug", Variant::BOOL, "Start the game in debug mode.", { "d", "debug", "debug-mode" }, "",
+ ArgumentOption::ARGUMENT_CONFLICT_SHORTHAND | ArgumentOption::ARGUMENT_CONFLICT_ALIASES
+ )
+ );
+ options.push_back(ArgumentOption::create("game-debug", Variant::BOOL, "Start the game in debug mode.", { "debug-mode" }));
+
options.push_back(
- ArgumentOption::create("game-debug", Variant::BOOL, "Start the game in debug mode.", { "d", "debug", "debug-mode" })
+ ArgumentOption::create(
+ "base-path", Variant::STRING, "Load Victoria 2 assets from a specific path.", { "b" }, "[path]",
+ ArgumentOption::ARGUMENT_CONFLICT_SHORTHAND
+ )
);
options.push_back(
- ArgumentOption::create("base-path", Variant::STRING, "Load Victoria 2 assets from a specific path.", { "b" }, "[path]")
+ ArgumentOption::create(
+ "search-path", Variant::STRING, "Search for Victoria 2 assets at a specific path.", { "s" }, "[path]",
+ ArgumentOption::ARGUMENT_CONFLICT_SHORTHAND
+ )
);
- options.push_back(ArgumentOption::create(
- "search-path", Variant::STRING, "Search for Victoria 2 assets at a specific path.", { "s" }, "[path]"
- ));
options.push_back(ArgumentOption::create("mod", Variant::PACKED_STRING_ARRAY, "Load Victoria 2 mods.", { "m" }, ""));
- parse_arguments(OS::get_singleton()->get_cmdline_args(), false);
+ parse_arguments(OS::get_singleton()->get_cmdline_args(), false, true);
parse_arguments(OS::get_singleton()->get_cmdline_user_args());
static const StringName ARGS_PATH = "openvic/data/arguments";
@@ -313,7 +342,7 @@ Variant ArgumentParser::get_option_value(StringName const& p_arg_name) const {
return it->value;
}
-void ArgumentParser::set_option_value(StringName const& p_arg_name, Variant p_value) {
+void ArgumentParser::set_option_value(StringName const& p_arg_name, Variant const& p_value) {
decltype(options)::Iterator it = _find_option(p_arg_name);
ERR_FAIL_COND_MSG(it == options.end(), vformat("No option name/alias found for '%s'.", p_arg_name));
@@ -338,10 +367,20 @@ String ArgumentParser::get_help(bool p_is_rich) const {
static const StringName APP_DESCRIPTION_PATH = "application/config/description";
PackedStringArray option_help;
- option_help.resize(options.size());
- for (size_t i = 0; String & help : option_help) {
- help = " " + options[i]->get_help_string(p_is_rich);
- i++;
+ {
+ HashSet handled_options;
+ handled_options.reserve(options.size());
+ option_help.resize(options.size());
+ for (size_t i = 0, help_index = 0; i < options.size(); i++, help_index++) {
+ StringName name = options[i]->get_name();
+ if (handled_options.has(name)) {
+ help_index--;
+ continue;
+ }
+ option_help[help_index] = " " + options[i]->get_help_string(p_is_rich);
+ handled_options.insert(name);
+ }
+ option_help.resize(handled_options.size());
}
String app_name = ProjectSettings::get_singleton()->get_setting_with_override(APP_NAME_PATH);
@@ -359,8 +398,7 @@ String ArgumentParser::get_help(bool p_is_rich) const {
app_description = vformat("[color=gray]%s[/color]", app_description);
}
- String app_copyright =
- "(c) " + convert_to(GAME_COPYRIGHT_INFO[0].parts.front().copyright_statements.front());
+ String app_copyright = "(c) " + convert_to(GAME_COPYRIGHT_INFO[0].parts.front().copyright_statements.front());
if (p_is_rich) {
app_copyright = vformat("[color=gray]%s[/color]", app_copyright);
}
@@ -415,9 +453,9 @@ decltype(ArgumentParser::options)::Iterator ArgumentParser::_find_option(godot::
}
Variant ArgumentParser::_parse_value( //
- StringName const& p_arg_name, String const& p_value_string, ArgumentOption const* p_option
+ StringName const& p_arg_name, String const& p_value_string, Ref& p_option
) {
- if (p_option == nullptr) {
+ if (p_option.is_null()) {
decltype(options)::Iterator it = _find_option(p_arg_name);
ERR_FAIL_COND_V_EDMSG(
it == options.end(), Variant(), vformat("Could not find option name/alias for '%s'.", p_arg_name)
@@ -534,85 +572,151 @@ Variant ArgumentParser::_parse_value( //
}
}
-Error ArgumentParser::parse_arguments(PackedStringArray const& p_args, bool p_error_unknown) {
+LocalVector[> ArgumentParser::_parse_argument_bool_list(String const& p_arg_list) {
+ LocalVector][> result;
+ result.reserve(p_arg_list.length());
+
+ for (size_t i = 0; i < p_arg_list.length(); i++) {
+ char32_t const& c = p_arg_list[i];
+ if (!is_ascii_alphabet_char(c)) {
+ break;
+ }
+
+ decltype(options)::Iterator it = _find_option(p_arg_list);
+ if (it == options.end()) {
+ WARN_PRINT(vformat("Shorthand alias '%s' not found, skipping.", c));
+ continue;
+ }
+
+ if (it->ptr()->get_type() == Variant::BOOL) {
+ result.push_back(it->ptr());
+ } else {
+ WARN_PRINT(vformat("Shorthand alias '%s' is not a boolean type, skipping.", c));
+ }
+ }
+
+ return result;
+}
+
+Pair, Variant> ArgumentParser::_parse_argument(
+ String const& p_argument, bool p_error_unknown, bool p_skip_conflict_args //
+) {
+ size_t equal_index = p_argument.find("=");
+ String key;
+ String value;
+ if (equal_index != -1) {
+ key = p_argument.substr(0, equal_index);
+ value = p_argument.substr(equal_index + 1);
+ } else {
+ key = p_argument;
+ }
+
+ if (key.begins_with("-")) {
+ if (key.length() == 2) {
+ WARN_PRINT(vformat("Invalid argument '%s' name is too short, skipping.", p_argument));
+ return {};
+ }
+ key = key.substr(1);
+ }
+
+ decltype(options)::Iterator it = _find_option(key);
+ if (it == options.end()) {
+ if (p_error_unknown) {
+ ERR_PRINT(vformat("Invalid argument '%s' found, skipping.", key));
+ }
+ return {};
+ }
+ ArgumentOption* option = it->ptr();
+
+ if (p_skip_conflict_args) {
+ const BitField conflict_type = option->get_conflict_type();
+ if (conflict_type.has_flag(ArgumentOption::ARGUMENT_CONFLICT_SHORTHAND) && key.length() == 1) {
+ return {};
+ }
+ if (conflict_type.has_flag(ArgumentOption::ARGUMENT_CONFLICT_LONG) && key.length() > 1) {
+ return {};
+ }
+ if (conflict_type.has_flag(ArgumentOption::ARGUMENT_CONFLICT_ALIASES) && option->has_alias(key)) {
+ return {};
+ }
+ }
+
+ if (equal_index != -1) {
+ Ref ref = option;
+ Variant arg_value = _parse_value(key, value, ref);
+ if (arg_value != Variant()) {
+ return { ref, arg_value };
+ }
+ } else if (option->get_type() == Variant::BOOL) {
+ return { option, true };
+ } else {
+ WARN_PRINT(vformat("Argument '%s' treated like a boolean but does not support a boolean value, skipping.", key));
+ }
+
+ return { option, {} };
+}
+
+Error ArgumentParser::parse_arguments(PackedStringArray const& p_args, bool p_error_unknown, bool p_skip_conflict_args) {
String key;
- ArgumentOption const* option = nullptr;
+ Ref option;
for (String const& arg : p_args) {
const bool arg_begins_with_minus = arg.begins_with("-");
- if (option != nullptr) {
+ if (option.is_valid()) {
if (!arg_begins_with_minus) {
Variant value = _parse_value(key, arg, option);
if (value != Variant()) {
arguments[option->get_name()] = value;
}
- option = nullptr;
+ option = {};
+ key = "";
continue;
+ } else if (option->get_type() == Variant::BOOL) {
+ // Boolean did not have an explicit value
+ option = {};
+ key = "";
} else {
WARN_PRINT(vformat("Valid argument '%s' was not set as a value, skipping.", key));
}
}
- if (arg_begins_with_minus) {
- String arg_name = arg.substr(1);
- if (arg_name.length() > 1 && arg_name[0] != U'-' and arg_name[1] != U'=') {
- for (size_t i = 0; i < arg_name.length(); i++) {
- char32_t const& c = arg_name[i];
- if (!((c >= U'a' && c <= U'z') || (c >= U'A' && c <= U'Z'))) {
- break;
- }
-
- decltype(options)::Iterator it = _find_option(arg_name);
- if (it == options.end()) {
- WARN_PRINT(vformat("Shorthand alias '%s' not found, skipping.", c));
- continue;
- }
-
- if (it->ptr()->get_type() == Variant::BOOL) {
- arguments[option->get_name()] = true;
- } else {
- WARN_PRINT(vformat("Shorthand alias '%s' is not a boolean type, skipping.", c));
- }
- }
- continue;
- }
-
- size_t equal_index = arg_name.find("=");
- String value;
- if (equal_index != -1) {
- key = arg_name.substr(0, equal_index);
- value = arg_name.substr(equal_index + 1);
- } else {
- key = arg_name;
+ if (!arg_begins_with_minus) {
+ if (p_error_unknown) {
+ ERR_PRINT(vformat("Unknown argument value '%s' found, skipping.", arg));
}
+ continue;
+ }
- if (key.length() > 2 && key.begins_with("-")) {
- key = key.substr(1);
- }
+ if (!option.is_null()) {
+ WARN_PRINT(vformat("Argument '%s' value expected, skipping.", key));
+ option = {};
+ key = "";
+ }
- decltype(options)::Iterator it = _find_option(key);
- if (it == options.end()) {
- if (p_error_unknown) {
- ERR_PRINT(vformat("Invalid argument '%s' found, skipping.", key));
- }
- continue;
+ key = arg.substr(1);
+ if (key.length() > 1 && key[0] != U'-' and key[1] != U'=') {
+ for (const Ref opt : _parse_argument_bool_list(key)) {
+ arguments[opt->get_name()] = true;
}
- option = it->ptr();
+ option = {};
+ continue;
+ }
- if (equal_index != -1) {
- Variant arg_value = _parse_value(key, value, option);
- if (arg_value != Variant()) {
- arguments[option->get_name()] = arg_value;
- option = nullptr;
- }
- } else if (option->get_type() == Variant::BOOL) {
- arguments[option->get_name()] = true;
- } else {
- WARN_PRINT(vformat("Argument '%s' treated like a boolean but does not support a boolean value, skipping.", key)
- );
- }
- } else if (p_error_unknown) {
- ERR_PRINT(vformat("Unknown argument '%s' found, skipping.", arg));
+ Pair, Variant> argument = _parse_argument(key, p_error_unknown, p_skip_conflict_args);
+ if (argument.first.is_null()) {
+ option = {};
+ continue;
+ } else if (argument.second == Variant {}) {
+ option = argument.first;
+ continue;
+ } else if (argument.first->get_type() == Variant::BOOL) {
+ // Boolean options could still have an explicit value
+ option = argument.first;
+ arguments[argument.first->get_name()] = argument.second;
+ continue;
+ } else {
+ arguments[argument.first->get_name()] = argument.second;
+ continue;
}
}
@@ -625,5 +729,7 @@ void ArgumentParser::_bind_methods() {
OV_BIND_METHOD(ArgumentParser::get_option_value, { "arg_name" });
OV_BIND_METHOD(ArgumentParser::set_option_value, { "arg_name", "value" });
OV_BIND_METHOD(ArgumentParser::get_help, { "is_rich" });
- OV_BIND_METHOD(ArgumentParser::parse_arguments, { "args", "error_unknown" }, DEFVAL(true));
+ OV_BIND_METHOD(
+ ArgumentParser::parse_arguments, { "args", "error_unknown", "skip_conflict_args" }, DEFVAL(true), DEFVAL(false)
+ );
}
diff --git a/extension/src/openvic-extension/core/ArgumentParser.hpp b/extension/src/openvic-extension/core/ArgumentParser.hpp
index e971b0a3..7113dff8 100644
--- a/extension/src/openvic-extension/core/ArgumentParser.hpp
+++ b/extension/src/openvic-extension/core/ArgumentParser.hpp
@@ -4,9 +4,12 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
+#include
#include
#include
#include
@@ -18,18 +21,29 @@ namespace OpenVic {
class ArgumentOption : public godot::RefCounted {
GDCLASS(ArgumentOption, godot::RefCounted);
+ public:
+ enum ArgumentConflictType {
+ ARGUMENT_CONFLICT_NONE = 0,
+ ARGUMENT_CONFLICT_SHORTHAND = 1 << 0,
+ ARGUMENT_CONFLICT_LONG = 1 << 1,
+ ARGUMENT_CONFLICT_ALIASES = 1 << 2
+ };
+
+ private:
godot::StringName name;
godot::String option_arguments;
godot::Variant default_value;
godot::String description;
godot::PackedStringArray aliases;
godot::HashSet aliases_set;
+ godot::BitField conflict_type = ARGUMENT_CONFLICT_NONE;
static godot::Variant _get_empty_value_for(godot::Variant::Type p_type);
ArgumentOption(
godot::StringName const& name, godot::Variant const& default_value, godot::String const& description,
- godot::PackedStringArray const& aliases, godot::String option_args
+ godot::PackedStringArray const& aliases, godot::String const& option_args,
+ godot::BitField conflict_type
);
protected:
@@ -49,16 +63,20 @@ namespace OpenVic {
godot::String get_description() const;
+ godot::BitField get_conflict_type() const;
+
godot::String get_help_string(bool p_is_rich = false) const;
static godot::Ref create(
godot::StringName const& p_name, godot::Variant::Type p_type, godot::String const& p_description,
- godot::PackedStringArray const& p_aliases = {}, godot::String p_option_args = godot::String()
+ godot::PackedStringArray const& p_aliases = {}, godot::String const& p_option_args = godot::String(),
+ godot::BitField p_conflict_type = ARGUMENT_CONFLICT_NONE
);
static godot::Ref create_with_default(
godot::StringName const& p_name, godot::Variant const& p_default, godot::String const& p_description,
- godot::PackedStringArray const& p_aliases = {}, godot::String p_option_args = godot::String()
+ godot::PackedStringArray const& p_aliases = {}, godot::String const& p_option_args = godot::String(),
+ godot::BitField p_conflict_type = ARGUMENT_CONFLICT_NONE
);
ArgumentOption() {}
@@ -77,8 +95,15 @@ namespace OpenVic {
decltype(options)::Iterator _find_option(godot::String const& p_arg_name);
+ godot::LocalVector> _parse_argument_bool_list(godot::String const& p_arg_list);
+
+ godot::Pair, godot::Variant> _parse_argument(
+ godot::String const& p_argument, bool p_error_unknown, bool p_skip_conflict_args //
+ );
+
godot::Variant _parse_value(
- godot::StringName const& p_arg_name, godot::String const& p_value_string, ArgumentOption const* p_option = nullptr
+ godot::StringName const& p_arg_name, godot::String const& p_value_string,
+ godot::Ref& p_option
);
protected:
@@ -92,13 +117,17 @@ namespace OpenVic {
bool has_option(godot::StringName const& p_arg_name, bool p_include_aliases = false) const;
bool is_option_set(godot::StringName const& p_arg_name, bool p_include_aliases = false) const;
godot::Variant get_option_value(godot::StringName const& p_arg_name) const;
- void set_option_value(godot::StringName const& p_arg_name, godot::Variant p_value);
+ void set_option_value(godot::StringName const& p_arg_name, godot::Variant const& p_value);
godot::String get_help(bool p_is_rich = false) const;
- godot::Error parse_arguments(godot::PackedStringArray const& p_args, bool p_error_unknown = true);
+ godot::Error parse_arguments(
+ godot::PackedStringArray const& p_args, bool p_error_unknown = true, bool p_skip_conflict_args = false //
+ );
ArgumentParser();
~ArgumentParser();
};
}
+
+VARIANT_BITFIELD_CAST(OpenVic::ArgumentOption::ArgumentConflictType);
]