From 865e7aeca7c3090fee914b9ebde9490ef23d3559 Mon Sep 17 00:00:00 2001 From: Spartan322 Date: Sat, 29 Apr 2023 02:49:48 -0400 Subject: 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 --- game/src/ArgumentOption.gd | 60 ++++++++++ game/src/ArgumentParser.gd | 259 +++++++++++++++++++++++++++++++++++++++++++ game/src/ArgumentParser.tscn | 24 ++++ game/src/GameStart.tscn | 5 +- 4 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 game/src/ArgumentOption.gd create mode 100644 game/src/ArgumentParser.gd create mode 100644 game/src/ArgumentParser.tscn (limited to 'game/src') 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 "" + +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 -- cgit v1.2.3-56-ga3b1