diff options
author | Spartan322 <Megacake1234@gmail.com> | 2023-04-29 08:49:48 +0200 |
---|---|---|
committer | Spartan322 <Megacake1234@gmail.com> | 2023-05-02 01:00:09 +0200 |
commit | 865e7aeca7c3090fee914b9ebde9490ef23d3559 (patch) | |
tree | 7836ab6e2e5bd751c226d86646883f8424577a7c /game/src | |
parent | 10053cf259c55ee45803268a844edf1011d8a16b (diff) |
Add ArgumentParser
Streamlines parsing of commandline arguments
Arguments reside in ProjectSettings as `openvic2/data/arguments` as a dictionary
The dictionary's key is the option name
The dictionary is set with default values
This enables project setting overrides for arguments (user specified arguments take priority)
Add help commandline option
Prepare for removal of GameDebug.gd
Add game project description
Diffstat (limited to 'game/src')
-rw-r--r-- | game/src/ArgumentOption.gd | 60 | ||||
-rw-r--r-- | game/src/ArgumentParser.gd | 259 | ||||
-rw-r--r-- | game/src/ArgumentParser.tscn | 24 | ||||
-rw-r--r-- | game/src/GameStart.tscn | 5 |
4 files changed, 347 insertions, 1 deletions
diff --git a/game/src/ArgumentOption.gd b/game/src/ArgumentOption.gd new file mode 100644 index 0000000..f14cef0 --- /dev/null +++ b/game/src/ArgumentOption.gd @@ -0,0 +1,60 @@ +@tool +class_name ArgumentOption +extends Resource + +@export var name : StringName +@export var aliases : Array[StringName] = [] +@export var type : Variant.Type : + get: return type + set(v): + type = v + match v: + TYPE_BOOL: default_value = false + TYPE_INT: default_value = 0 + TYPE_FLOAT: default_value = 0.0 + TYPE_STRING: default_value = "" + TYPE_STRING_NAME: default_value = &"" + TYPE_COLOR: default_value = Color() + _: default_value = null + notify_property_list_changed() +var default_value +@export var description : String + +func _init(_name = "", _type = TYPE_NIL, _description = "", default = null): + name = _name + type = _type + if default != null and typeof(default) == type: + default_value = default + description = _description + +func add_alias(alias : StringName) -> ArgumentOption: + aliases.append(alias) + return self + +func get_type_string() -> StringName: + match type: + TYPE_NIL: return "null" + TYPE_BOOL: return "boolean" + TYPE_INT: return "integer" + TYPE_FLOAT: return "float" + TYPE_STRING, TYPE_STRING_NAME: return "string" + TYPE_COLOR: return "color" + return "<invalid type>" + +func _get(property): + if property == "default_value": return default_value + +func _set(property, value): + if property == "default_value": + default_value = value + return true + +func _get_property_list(): + var properties := [] + + properties.append({ + "name": "default_value", + "type": type + }) + + return properties diff --git a/game/src/ArgumentParser.gd b/game/src/ArgumentParser.gd new file mode 100644 index 0000000..3f77919 --- /dev/null +++ b/game/src/ArgumentParser.gd @@ -0,0 +1,259 @@ +@tool +extends Node + +const argument_setting_path := &"openvic2/data/arguments" + +@export var option_array : Array[ArgumentOption] = [ + ArgumentOption.new( + "help", + TYPE_BOOL, + "Displays help and quits.", + false + ).add_alias(&"h") +] + +const color_name_array : PackedStringArray =[ + "aliceblue", "antiquewhite", "aqua", "aquamarine", + "azure", "beige", "bisque", "black", "blanchedalmond", + "blue", "blueviolet", "brown", "burlywood", "cadetblue", + "chartreuse", "chocolate", "coral", "cornflower", "cornsilk", + "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", + "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", + "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", + "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", + "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", + "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", + "gold", "goldenrod", "gray", "green", "greenyellow", "honeydew", + "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", + "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", + "lightcyan", "lightgoldenrod", "lightgray", "lightgreen", "lightpink", + "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", + "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", + "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", + "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", + "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream", + "mistyrose", "moccasin", "navajowhite", "navyblue", "oldlace", "olive", + "olivedrab", "orange", "orangered", "orchid", "palegoldenrod", + "palegreen", "paleturquoise", "palevioletred", "papayawhip", + "peachpuff", "peru", "pink", "plum", "powderblue", "purple", + "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", + "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", + "skyblue", "slateblue", "slategray", "snow", "springgreen", "steelblue", + "tan", "teal", "thistle", "tomato", "transparent", "turquoise", "violet", + "webgray", "webgreen", "webmaroon", "webpurple", "wheat", "white", + "whitesmoke", "yellow", "yellowgreen" +] + +func _parse_value(arg_name : StringName, value_string : String, type : Variant.Type) -> Variant: + match type: + TYPE_NIL: return null + TYPE_BOOL: + value_string = value_string.to_lower() + if value_string == "true" or value_string == "t" or value_string == "yes" or value_string == "y": + return true + if value_string == "false" or value_string == "f" or value_string == "no" or value_string == "n": + return false + return true + TYPE_INT: + return value_string.to_int() + TYPE_FLOAT: + return value_string.to_float() + TYPE_STRING, TYPE_STRING_NAME: + return value_string + TYPE_COLOR: + if Color.html_is_valid(value_string) or value_string.to_lower() in color_name_array: + return Color.from_string(value_string, Color()) + push_error("'%s' must be an html Color or Color name, '%s' is an invalid value." % [arg_name, value_string]) + return null + # Unsupported types + TYPE_VECTOR2, \ + TYPE_VECTOR2I, \ + TYPE_VECTOR3, \ + TYPE_VECTOR3I, \ + TYPE_VECTOR4, \ + TYPE_VECTOR4I, \ + TYPE_RECT2, \ + TYPE_RECT2I: + push_warning("Value type '%s' may not be supported." % type) + var data_array = value_string.lstrip("(").rstrip(")").split(",", false) + for index in range(data_array.size()): + data_array[index] = " " + data_array[index].strip_edges() + match type: + TYPE_VECTOR2: + if data_array.size() != 2: + push_error("'%s' value must be a Vector2, '%s' is an invalid value." % [arg_name, value_string]) + return null + return str_to_var("Vector2(%s )" % ",".join(data_array)) + TYPE_VECTOR2I: + if data_array.size() != 2: + push_error("'%s' value must be a Vector2i, '%s' is an invalid value." % [arg_name, value_string]) + return null + return str_to_var("Vector2i(%s )" % ",".join(data_array)) + TYPE_VECTOR3: + if data_array.size() != 2: + push_error("'%s' value must be a Vector3, '%s' is an invalid value." % [arg_name, value_string]) + return null + return str_to_var("Vector3(%s )" % ",".join(data_array)) + TYPE_VECTOR3I: + if data_array.size() != 2: + push_error("'%s' value must be a Vector3i, '%s' is an invalid value." % [arg_name, value_string]) + return null + return str_to_var("Vector3i(%s )" % ",".join(data_array)) + TYPE_VECTOR4: + if data_array.size() != 2: + push_error("'%s' value must be a Vector4, '%s' is an invalid value." % [arg_name, value_string]) + return null + return str_to_var("Vector4(%s )" % ",".join(data_array)) + TYPE_VECTOR4I: + if data_array.size() != 2: + push_error("'%s' value must be a Vector4i, '%s' is an invalid value." % [arg_name, value_string]) + return null + return str_to_var("Vector4i(%s )" % ",".join(data_array)) + TYPE_RECT2: + if data_array.size() != 2: + push_error("'%s' value must be a Rect2, '%s' is an invalid value." % [arg_name, value_string]) + return null + return str_to_var("Rect2(%s )" % ",".join(data_array)) + TYPE_RECT2I: + if data_array.size() != 2: + push_error("'%s' value must be a Rect2i, '%s' is an invalid value." % [arg_name, value_string]) + return null + return str_to_var("Rect2i(%s )" % ",".join(data_array)) + _: + push_error("'%s' value of type '%s' requested but could not be parsed." % [arg_name, type]) + return null + + return null + +# Missing types +# TYPE_TRANSFORM2D = 11 +# TYPE_VECTOR4 = 12 +# TYPE_VECTOR4I = 13 +# TYPE_PLANE = 14 +# TYPE_QUATERNION = 15 +# TYPE_AABB = 16 +# TYPE_BASIS = 17 +# TYPE_TRANSFORM3D = 18 +# TYPE_PROJECTION = 19 +# TYPE_NODE_PATH = 22 +# TYPE_RID = 23 +# TYPE_OBJECT = 24 +# TYPE_CALLABLE = 25 +# TYPE_SIGNAL = 26 +# TYPE_DICTIONARY = 27 +# TYPE_ARRAY = 28 +# TYPE_PACKED_BYTE_ARRAY = 29 +# TYPE_PACKED_INT32_ARRAY = 30 +# TYPE_PACKED_INT64_ARRAY = 31 +# TYPE_PACKED_FLOAT32_ARRAY = 32 +# TYPE_PACKED_FLOAT64_ARRAY = 33 +# TYPE_PACKED_STRING_ARRAY = 34 +# TYPE_PACKED_VECTOR2_ARRAY = 35 +# TYPE_PACKED_VECTOR3_ARRAY = 36 +# TYPE_PACKED_COLOR_ARRAY = 37 + +func _parse_argument_list(dictionary : Dictionary, arg_list : PackedStringArray) -> Dictionary: + var current_key : String = "" + var current_option : ArgumentOption = null + for arg in arg_list: + if current_option != null and not arg.begins_with("-"): + var result = _parse_value(current_key, arg, current_option.type) + if result != null: + dictionary[current_option.name] = result + current_option = null + continue + + if current_option != null: + push_warning("Valid argument '%s' was not set as a value, skipping." % current_key) + + if arg.begins_with("-"): + current_option = null + arg = arg.substr(1) + var key := &"" + var value := &"" + + # Support for Unix shorthand of multiple boolean arguments + # eg: "-abc" means a == true, b == true, c == true + if arg.length() > 1 and arg[0] != "-" and arg[1] != "=": + for c in arg: + for o in option_array: + if o.aliases.any(func(v): return c == v): + dictionary[o.name] = true + continue + + # Support for = key/value split + # eg: "-v=5" and "--value=5" means v == 5 and value == 5 + var first_equal := arg.find("=") + if first_equal > -1: + key = arg.substr(0, first_equal - 1) + value = arg.substr(first_equal + 1) + else: + key = arg + + # Removes - for full name arguments + if key.begins_with("-"): + key = key.substr(1) + + for o in option_array: + if key == o.name or o.aliases.any(func(v): return key == v): + current_option = o + break + + if current_option == null: + push_warning("Invalid argument '%s' found, skipping." % key) + continue + + current_key = key + var arg_result = _parse_value(key, value, current_option.type) + if arg_result != null: + dictionary[current_option.name] = arg_result + current_option = null + + return dictionary + +func _print_help(): + var project_name : StringName = ProjectSettings.get_setting_with_override(&"application/config/name") + var project_version : String = _GIT_INFO_.tag + var project_hash : String = _GIT_INFO_.short_hash + var project_website : String = "https://openvic2.com" + var project_description : String = ProjectSettings.get_setting_with_override(&"application/config/description") + print_rich( +""" +%s - %s - %s - %s +%s + +%s + +Options: +""" + % [ + project_name, + project_version, + project_hash, + project_website, + project_description, + "usage: %s [options]" % OS.get_executable_path().get_file() + ] + ) + for option in option_array: + print_rich(" --%s%s%s" % [ + (option.name + (",-%s" % (",-".join(option.aliases)) if option.aliases.size() > 0 else "")).rpad(45), + ("Type: %s - Default Value: %s" % [option.get_type_string(), option.default_value]).rpad(45), + option.description + ]) +func _ready(): + if Engine.is_editor_hint(): return + + var argument_dictionary : Dictionary = {} + if ProjectSettings.has_setting(argument_setting_path): + argument_dictionary = ProjectSettings.get_setting_with_override(argument_setting_path) + for option in option_array: + argument_dictionary[option.name] = option.default_value + + _parse_argument_list(argument_dictionary, OS.get_cmdline_args()) + _parse_argument_list(argument_dictionary, OS.get_cmdline_user_args()) + + ProjectSettings.set_setting(argument_setting_path, argument_dictionary) + if argument_dictionary[&"help"]: + _print_help() + get_tree().quit() diff --git a/game/src/ArgumentParser.tscn b/game/src/ArgumentParser.tscn new file mode 100644 index 0000000..964c19e --- /dev/null +++ b/game/src/ArgumentParser.tscn @@ -0,0 +1,24 @@ +[gd_scene load_steps=5 format=3 uid="uid://dayjmgc34tqo6"] + +[ext_resource type="Script" path="res://src/ArgumentParser.gd" id="1_8e7gi"] +[ext_resource type="Script" path="res://src/ArgumentOption.gd" id="2_f3c26"] + +[sub_resource type="Resource" id="Resource_tq3y4"] +script = ExtResource("2_f3c26") +name = &"help" +aliases = Array[StringName]([&"h"]) +type = 1 +description = "Displays help and quits." +default_value = false + +[sub_resource type="Resource" id="Resource_j1to4"] +script = ExtResource("2_f3c26") +name = &"game-debug" +aliases = Array[StringName]([&"d", &"-debug", &"-debug-mode"]) +type = 1 +description = "Start in debug mode." +default_value = false + +[node name="ArgumentParser" type="Node"] +script = ExtResource("1_8e7gi") +option_array = Array[ExtResource("2_f3c26")]([SubResource("Resource_tq3y4"), SubResource("Resource_j1to4")]) diff --git a/game/src/GameStart.tscn b/game/src/GameStart.tscn index 2046bb5..d5f9d45 100644 --- a/game/src/GameStart.tscn +++ b/game/src/GameStart.tscn @@ -1,5 +1,6 @@ -[gd_scene load_steps=6 format=3 uid="uid://1udsn4mggep2"] +[gd_scene load_steps=7 format=3 uid="uid://1udsn4mggep2"] +[ext_resource type="PackedScene" uid="uid://dayjmgc34tqo6" path="res://src/ArgumentParser.tscn" id="1_oe61r"] [ext_resource type="PackedScene" uid="uid://o4u142w4qkln" path="res://src/GameMenu.tscn" id="1_wlojq"] [ext_resource type="Script" path="res://src/SplashContainer.gd" id="2_xmcgv"] [ext_resource type="Texture2D" uid="uid://deef5hufq0j61" path="res://splash_assets/splash_end.png" id="3_qfv12"] @@ -14,6 +15,8 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +[node name="ArgumentParser" parent="." instance=ExtResource("1_oe61r")] + [node name="GameMenu" parent="." instance=ExtResource("1_wlojq")] visible = false layout_mode = 1 |