aboutsummaryrefslogtreecommitdiff
path: root/game/src/Game/Autoload
diff options
context:
space:
mode:
Diffstat (limited to 'game/src/Game/Autoload')
-rw-r--r--game/src/Game/Autoload/Argument/ArgumentOption.gd60
-rw-r--r--game/src/Game/Autoload/Argument/ArgumentParser.gd267
-rw-r--r--game/src/Game/Autoload/Argument/ArgumentParser.tscn32
-rw-r--r--game/src/Game/Autoload/Events.gd45
-rw-r--r--game/src/Game/Autoload/Events/GameDebug.gd21
-rw-r--r--game/src/Game/Autoload/Events/Localisation.gd30
-rw-r--r--game/src/Game/Autoload/Events/Options.gd30
-rw-r--r--game/src/Game/Autoload/Events/ShaderManager.gd48
-rw-r--r--game/src/Game/Autoload/GuiScale.gd62
-rw-r--r--game/src/Game/Autoload/Resolution.gd101
-rw-r--r--game/src/Game/Autoload/SaveManager.gd55
-rw-r--r--game/src/Game/Autoload/SoundManager.gd42
12 files changed, 793 insertions, 0 deletions
diff --git a/game/src/Game/Autoload/Argument/ArgumentOption.gd b/game/src/Game/Autoload/Argument/ArgumentOption.gd
new file mode 100644
index 0000000..f14cef0
--- /dev/null
+++ b/game/src/Game/Autoload/Argument/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/Game/Autoload/Argument/ArgumentParser.gd b/game/src/Game/Autoload/Argument/ArgumentParser.gd
new file mode 100644
index 0000000..ce89dd8
--- /dev/null
+++ b/game/src/Game/Autoload/Argument/ArgumentParser.gd
@@ -0,0 +1,267 @@
+@tool
+extends Node
+
+const argument_setting_path := &"openvic/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
+ push_error("'%s' must be a valid boolean, '%s' is an invalid value." % [arg_name, value_string])
+ return null
+ TYPE_INT:
+ if value_string.is_valid_int():
+ return value_string.to_int()
+ push_error("'%s' must be a valid integer, '%s' is an invalid value." % [arg_name, value_string])
+ return null
+ TYPE_FLOAT:
+ if value_string.is_valid_float():
+ return value_string.to_float()
+ push_error("'%s' must be a valid float, '%s' is an invalid value." % [arg_name, value_string])
+ return null
+ 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
+ if first_equal > -1:
+ 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://openvic.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/Game/Autoload/Argument/ArgumentParser.tscn b/game/src/Game/Autoload/Argument/ArgumentParser.tscn
new file mode 100644
index 0000000..09e5e6c
--- /dev/null
+++ b/game/src/Game/Autoload/Argument/ArgumentParser.tscn
@@ -0,0 +1,32 @@
+[gd_scene load_steps=6 format=3 uid="uid://dayjmgc34tqo6"]
+
+[ext_resource type="Script" path="res://src/Game/Autoload/Argument/ArgumentParser.gd" id="1_pc7xr"]
+[ext_resource type="Script" path="res://src/Game/Autoload/Argument/ArgumentOption.gd" id="2_4hguj"]
+
+[sub_resource type="Resource" id="Resource_tq3y4"]
+script = ExtResource("2_4hguj")
+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_4hguj")
+name = &"game-debug"
+aliases = Array[StringName]([&"d", &"-debug", &"-debug-mode"])
+type = 1
+description = "Start in debug mode."
+default_value = false
+
+[sub_resource type="Resource" id="Resource_tiax1"]
+script = ExtResource("2_4hguj")
+name = &"compatibility-mode"
+aliases = Array[StringName]([&"-compat"])
+type = 4
+description = "Load Victoria 2 assets from this path."
+default_value = ""
+
+[node name="ArgumentParser" type="Node"]
+script = ExtResource("1_pc7xr")
+option_array = Array[ExtResource("2_4hguj")]([SubResource("Resource_tq3y4"), SubResource("Resource_j1to4"), SubResource("Resource_tiax1")])
diff --git a/game/src/Game/Autoload/Events.gd b/game/src/Game/Autoload/Events.gd
new file mode 100644
index 0000000..4387cc7
--- /dev/null
+++ b/game/src/Game/Autoload/Events.gd
@@ -0,0 +1,45 @@
+extends Node
+
+var GameDebug = preload("Events/GameDebug.gd").new()
+var Options = preload("Events/Options.gd").new()
+var Localisation = preload("Events/Localisation.gd").new()
+var ShaderManager = preload("Events/ShaderManager.gd").new()
+
+var _define_filepaths_dict : Dictionary = {
+ GameSingleton.get_province_identifier_file_key(): "res://common/map/provinces.json",
+ GameSingleton.get_water_province_file_key(): "res://common/map/water.json",
+ GameSingleton.get_region_file_key(): "res://common/map/regions.json",
+ GameSingleton.get_terrain_variant_file_key(): "res://common/map/terrain.json",
+ GameSingleton.get_terrain_texture_dir_key(): "res://art/terrain/",
+ GameSingleton.get_province_image_file_key(): "res://common/map/provinces.png",
+ GameSingleton.get_terrain_image_file_key(): "res://common/map/terrain.png",
+ GameSingleton.get_goods_file_key(): "res://common/goods.json",
+ GameSingleton.get_good_icons_dir_key(): "res://art/economy/goods"
+}
+
+# REQUIREMENTS
+# * FS-333, FS-334, FS-335, FS-341
+func _ready():
+ GameSingleton.setup_logger()
+
+ # Set this to your Vic2 install dir or a mod's dir to enable compatibility mode
+ # (this won't work for mods which rely on vanilla map assets, copy missing assets
+ # into the mod's dir for a temporary fix)
+ # Usage: OpenVic --compatibility-mode <path>
+
+ var compatibility_mode_path : String
+ if ProjectSettings.has_setting(ArgumentParser.argument_setting_path):
+ var arg_dictionary : Dictionary = ProjectSettings.get_setting(ArgumentParser.argument_setting_path)
+ compatibility_mode_path = arg_dictionary.get(&"compatibility-mode", compatibility_mode_path)
+
+ var start := Time.get_ticks_usec()
+
+ if compatibility_mode_path:
+ if GameSingleton.load_defines_compatibility_mode(compatibility_mode_path) != OK:
+ push_error("Errors loading game defines!")
+ else:
+ if GameSingleton.load_defines(_define_filepaths_dict) != OK:
+ push_error("Errors loading game defines!")
+
+ var end := Time.get_ticks_usec()
+ print("Loading took ", float(end - start) / 1000000, " seconds")
diff --git a/game/src/Game/Autoload/Events/GameDebug.gd b/game/src/Game/Autoload/Events/GameDebug.gd
new file mode 100644
index 0000000..df7a23a
--- /dev/null
+++ b/game/src/Game/Autoload/Events/GameDebug.gd
@@ -0,0 +1,21 @@
+extends RefCounted
+
+# REQUIREMENTS:
+# * SS-56
+func _init():
+ for engine_args in OS.get_cmdline_args():
+ match(engine_args):
+ "--game-debug":
+ set_debug_mode(true)
+
+ for engine_args in OS.get_cmdline_user_args():
+ match(engine_args):
+ "--game-debug", "-d", "--debug", "--debug-mode":
+ set_debug_mode(true)
+
+func set_debug_mode(value : bool) -> void:
+ ProjectSettings.set_setting("openvic/debug/enabled", value)
+ print("Set debug mode to: ", value)
+
+func is_debug_mode() -> bool:
+ return ProjectSettings.get_setting("openvic/debug/enabled", false)
diff --git a/game/src/Game/Autoload/Events/Localisation.gd b/game/src/Game/Autoload/Events/Localisation.gd
new file mode 100644
index 0000000..eda7e51
--- /dev/null
+++ b/game/src/Game/Autoload/Events/Localisation.gd
@@ -0,0 +1,30 @@
+extends RefCounted
+
+# REQUIREMENTS
+# * SS-59, SS-60, SS-61
+func get_default_locale() -> String:
+ var locales := TranslationServer.get_loaded_locales()
+ var default_locale := OS.get_locale()
+ if default_locale in locales:
+ return default_locale
+ var default_language := OS.get_locale_language()
+ for locale in locales:
+ if locale.begins_with(default_language):
+ return default_language
+ return ProjectSettings.get_setting("internationalization/locale/fallback", "en_GB")
+
+func load_localisation(dir_path : String) -> void:
+ if LoadLocalisation.load_localisation_dir(dir_path) == OK:
+ print("loaded locales: ", TranslationServer.get_loaded_locales())
+ else:
+ push_error("Failed to load localisation directory: ", dir_path)
+
+# REQUIREMENTS
+# * SS-57
+# * FS-17
+func _init():
+ var localisation_dir_path : String = ProjectSettings.get_setting("internationalization/locale/localisation_path", "")
+ if localisation_dir_path.is_empty():
+ push_error("Missing localisation_path setting!")
+ else:
+ load_localisation(localisation_dir_path)
diff --git a/game/src/Game/Autoload/Events/Options.gd b/game/src/Game/Autoload/Events/Options.gd
new file mode 100644
index 0000000..fbeccef
--- /dev/null
+++ b/game/src/Game/Autoload/Events/Options.gd
@@ -0,0 +1,30 @@
+extends RefCounted
+
+signal save_settings(save_file: ConfigFile)
+signal load_settings(load_file: ConfigFile)
+signal reset_settings()
+
+func load_settings_from_file() -> void:
+ load_settings.emit(_settings_file)
+
+# REQUIREMENTS
+# * SS-11
+func save_settings_to_file() -> void:
+ save_settings.emit(_settings_file)
+ _settings_file.save(_settings_file_path)
+
+func try_reset_settings() -> void:
+ reset_settings.emit()
+
+const settings_file_path_setting : String = "openvic/settings/settings_file_path"
+const settings_file_path_default : String = "user://settings.cfg"
+
+var _settings_file_path : String = ProjectSettings.get_setting(settings_file_path_setting, settings_file_path_default)
+var _settings_file := ConfigFile.new()
+
+# REQUIREMENTS
+# * SS-9
+# * UIFUN-7
+func _init():
+ if FileAccess.file_exists(_settings_file_path):
+ _settings_file.load(_settings_file_path)
diff --git a/game/src/Game/Autoload/Events/ShaderManager.gd b/game/src/Game/Autoload/Events/ShaderManager.gd
new file mode 100644
index 0000000..a503c52
--- /dev/null
+++ b/game/src/Game/Autoload/Events/ShaderManager.gd
@@ -0,0 +1,48 @@
+extends RefCounted
+
+const param_province_shape_tex : StringName = &"province_shape_tex"
+const param_province_shape_subdivisions : StringName = &"province_shape_subdivisions"
+const param_province_colour_tex : StringName = &"province_colour_tex"
+const param_hover_index : StringName = &"hover_index"
+const param_selected_index : StringName = &"selected_index"
+const param_terrain_tex : StringName = &"terrain_tex"
+const param_terrain_tile_factor : StringName = &"terrain_tile_factor"
+
+func set_up_shader(material : Material, add_cosmetic_textures : bool) -> Error:
+ # Shader Material
+ if material == null:
+ push_error("material is null!")
+ return FAILED
+ if not material is ShaderMaterial:
+ push_error("Invalid map mesh material class: ", material.get_class())
+ return FAILED
+ var shader_material : ShaderMaterial = material
+
+ # Province shape texture
+ var province_shape_texture := GameSingleton.get_province_shape_texture()
+ if province_shape_texture == null:
+ push_error("Failed to get province shape texture!")
+ return FAILED
+ shader_material.set_shader_parameter(param_province_shape_tex, province_shape_texture)
+ var subdivisions := GameSingleton.get_province_shape_image_subdivisions()
+ if subdivisions.x < 1 or subdivisions.y < 1:
+ push_error("Invalid province shape image subdivision: ", subdivisions.x, "x", subdivisions.y)
+ return FAILED
+ shader_material.set_shader_parameter(param_province_shape_subdivisions, Vector2(subdivisions))
+
+ if add_cosmetic_textures:
+ # Province colour texture
+ var map_province_colour_texture := GameSingleton.get_province_colour_texture()
+ if map_province_colour_texture == null:
+ push_error("Failed to get province colour image!")
+ return FAILED
+ shader_material.set_shader_parameter(param_province_colour_tex, map_province_colour_texture)
+
+ # Terrain texture
+ var terrain_texture := GameSingleton.get_terrain_texture()
+ if terrain_texture == null:
+ push_error("Failed to get terrain texture!")
+ return FAILED
+ shader_material.set_shader_parameter(param_terrain_tex, terrain_texture)
+
+ return OK
diff --git a/game/src/Game/Autoload/GuiScale.gd b/game/src/Game/Autoload/GuiScale.gd
new file mode 100644
index 0000000..afd73df
--- /dev/null
+++ b/game/src/Game/Autoload/GuiScale.gd
@@ -0,0 +1,62 @@
+extends Node
+
+const error_guiscale : float = -1.0
+
+@export
+var minimum_guiscale : float = 0.1
+
+const _starting_guiscales : Dictionary = {
+ float(0.5) : &"0.5x",
+ float(0.75): &"0.75x",
+ float(1) : &"1x",
+ float(1.5) : &"1.5x",
+ float(2) : &"2x",
+}
+
+var _guiscales: Dictionary
+
+#Similar to Resolution.gd, but we don't bother checking for strings from files
+#and we have floats instead of vector2 integers
+
+func _ready():
+ assert(minimum_guiscale > 0, "Minimum gui scale must be positive")
+ for guiscale_value in _starting_guiscales:
+ add_guiscale(guiscale_value, _starting_guiscales[guiscale_value])
+ assert(not _guiscales.is_empty(), "No valid starting gui scales!")
+
+func has_guiscale(guiscale_value : float) -> bool:
+ return guiscale_value in _guiscales
+
+func add_guiscale(guiscale_value: float, guiscale_name: StringName=&"") -> bool:
+ if has_guiscale(guiscale_value): return true
+ var scale_dict := { value = guiscale_value }
+ if not guiscale_name.is_empty():
+ scale_dict.display_name = guiscale_name
+ else:
+ scale_dict.display_name = StringName("%sx" % guiscale_value)
+ if guiscale_value < minimum_guiscale:
+ push_error("GUI scale %s is smaller than the minimum %s" % [scale_dict.display_name, minimum_guiscale])
+ return false
+ _guiscales[guiscale_value] = scale_dict
+ return true
+
+#returns floats
+func get_guiscale_value_list() -> Array:
+ var list := _guiscales.keys()
+ list.sort_custom(func(a, b): return a > b)
+ return list
+
+func get_guiscale_display_name(guiscale_value : float) -> StringName:
+ return _guiscales.get(guiscale_value, {display_name = &"unknown gui scale"}).display_name
+
+func get_current_guiscale() -> float:
+ return get_tree().root.content_scale_factor
+
+func set_guiscale(guiscale:float) -> void:
+ print("New GUI scale: %f" % guiscale)
+ if not has_guiscale(guiscale):
+ push_warning("Setting GUI Scale to non-standard value %sx" % [guiscale])
+ get_tree().root.content_scale_factor = guiscale
+
+func reset_guiscale() -> void:
+ set_guiscale(get_current_guiscale())
diff --git a/game/src/Game/Autoload/Resolution.gd b/game/src/Game/Autoload/Resolution.gd
new file mode 100644
index 0000000..c973ba9
--- /dev/null
+++ b/game/src/Game/Autoload/Resolution.gd
@@ -0,0 +1,101 @@
+extends Node
+
+signal resolution_added(value : Vector2i, name : StringName, display_name : StringName)
+signal resolution_changed(value : Vector2i)
+signal window_mode_changed(value : Window.Mode)
+
+const error_resolution : Vector2i = Vector2i(-1,-1)
+
+@export
+var minimum_resolution : Vector2i = Vector2i(1,1)
+
+const _starting_resolutions : Dictionary = {
+ Vector2i(3840,2160): &"4K",
+ Vector2i(2560,1080): &"UW1080p",
+ Vector2i(1920,1080): &"1080p",
+ Vector2i(1366,768) : &"",
+ Vector2i(1536,864) : &"",
+ Vector2i(1280,720) : &"720p",
+ Vector2i(1440,900) : &"",
+ Vector2i(1600,900) : &"",
+ Vector2i(1024,600) : &"",
+ Vector2i(800,600) : &""
+}
+
+var _resolutions : Dictionary
+
+const _regex_pattern : String = "(\\d+)\\s*[xX,]\\s*(\\d+)"
+var _regex : RegEx
+
+func _ready():
+ assert(minimum_resolution.x > 0 and minimum_resolution.y > 0, "Minimum resolution must be positive!")
+ for resolution_value in _starting_resolutions:
+ add_resolution(resolution_value, _starting_resolutions[resolution_value])
+ assert(not _resolutions.is_empty(), "No valid starting resolutions!")
+
+ _regex = RegEx.new()
+ var err := _regex.compile(_regex_pattern)
+ assert(err == OK, "Resolution RegEx failed to compile!")
+
+
+func has_resolution(resolution_value : Vector2i) -> bool:
+ return resolution_value in _resolutions
+
+func add_resolution(resolution_value : Vector2i, resolution_name : StringName = &"") -> bool:
+ if has_resolution(resolution_value): return true
+ var res_dict := { value = resolution_value, name = &"" }
+ var display_name := "%sx%s" % [resolution_value.x, resolution_value.y]
+ if not resolution_name.is_empty():
+ res_dict.name = resolution_name
+ display_name = "%s (%s)" % [display_name, resolution_name]
+ res_dict.display_name = StringName(display_name)
+ if resolution_value.x < minimum_resolution.x or resolution_value.y < minimum_resolution.y:
+ push_error("Resolution %s is smaller than minimum (%sx%s)" % [res_dict.display_name, minimum_resolution.x, minimum_resolution.y])
+ return false
+ resolution_added.emit(resolution_value, resolution_name, display_name)
+ _resolutions[resolution_value] = res_dict
+ return true
+
+func get_resolution_value_list() -> Array:
+ var list := _resolutions.keys()
+ list.sort_custom(func(a, b): return a > b)
+ return list
+
+func get_resolution_name(resolution_value : Vector2i) -> StringName:
+ return _resolutions.get(resolution_value, { name = &"unknown resolution" }).name
+
+func get_resolution_display_name(resolution_value : Vector2i) -> StringName:
+ return _resolutions.get(resolution_value, { display_name = &"unknown resolution" }).display_name
+
+func get_resolution_value_from_string(resolution_string : String) -> Vector2i:
+ if not resolution_string.is_empty():
+ for resolution in _resolutions.values():
+ if resolution_string == resolution.name or resolution_string == resolution.display_name:
+ return resolution.value
+ var result := _regex.search(resolution_string)
+ if result: return Vector2i(result.get_string(1).to_int(), result.get_string(2).to_int())
+ return error_resolution
+
+func get_current_resolution() -> Vector2i:
+ var window := get_viewport().get_window()
+ match window.mode:
+ Window.MODE_EXCLUSIVE_FULLSCREEN, Window.MODE_FULLSCREEN:
+ return window.content_scale_size
+ _:
+ return window.size
+
+func set_resolution(resolution : Vector2i) -> void:
+ if not has_resolution(resolution):
+ push_warning("Setting resolution to non-standard value %sx%s" % [resolution.x, resolution.y])
+ var window := get_viewport().get_window()
+ if get_current_resolution() != resolution:
+ resolution_changed.emit(resolution)
+ match window.mode:
+ Window.MODE_EXCLUSIVE_FULLSCREEN, Window.MODE_FULLSCREEN:
+ window.content_scale_size = resolution
+ _:
+ window.size = resolution
+ window.content_scale_size = Vector2i(0,0)
+
+func reset_resolution() -> void:
+ set_resolution(get_current_resolution())
diff --git a/game/src/Game/Autoload/SaveManager.gd b/game/src/Game/Autoload/SaveManager.gd
new file mode 100644
index 0000000..fb7806b
--- /dev/null
+++ b/game/src/Game/Autoload/SaveManager.gd
@@ -0,0 +1,55 @@
+extends Node
+
+# Requirements
+# * FS-28
+const save_directory_setting := &"openvic/data/saves_directory"
+
+var current_save : SaveResource
+var current_session_tag : StringName
+
+var _save_dictionary : Dictionary = {}
+var _dirty_save : SaveResource
+
+func _ready():
+ var saves_dir_path : String = ProjectSettings.get_setting_with_override(save_directory_setting)
+ assert(saves_dir_path != null, "'%s' setting could not be found." % save_directory_setting)
+
+ DirAccess.make_dir_recursive_absolute(saves_dir_path)
+ var saves_dir := DirAccess.open(saves_dir_path)
+ for file in saves_dir.get_files():
+ var save := SaveResource.new()
+ save.load_save(saves_dir_path.path_join(file))
+ add_or_replace_save(save, true)
+
+func get_save_file_name(save_name : StringName, session_tag : StringName = current_session_tag) -> StringName:
+ return ("%s - %s" % [save_name, session_tag]).validate_filename()
+
+func make_new_save(save_name : String, session_tag : StringName = current_session_tag) -> SaveResource:
+ var file_name := get_save_file_name(save_name, session_tag) + ".tres"
+ var new_save := SaveResource.new()
+ new_save.set_file_path(save_name, ProjectSettings.get_setting_with_override(save_directory_setting).path_join(file_name))
+ print(new_save.file_path)
+ new_save.session_tag = session_tag
+ return new_save
+
+func has_save(save_name : StringName, session_tag : StringName = current_session_tag) -> bool:
+ return _save_dictionary.has(get_save_file_name(save_name, session_tag))
+
+func add_or_replace_save(save : SaveResource, ignore_dirty : bool = false) -> void:
+ var binded_func := _on_save_deleted_or_moved.bind(save)
+ save.deleted.connect(binded_func)
+ save.trash_moved.connect(binded_func)
+ _save_dictionary[get_save_file_name(save.save_name, save.session_tag)] = save
+ if not ignore_dirty:
+ _dirty_save = save
+
+func delete_save(save : SaveResource) -> void:
+ save.delete()
+
+func flush_save() -> void:
+ if _dirty_save == null: return
+ _dirty_save.flush_save()
+ _dirty_save = null
+
+func _on_save_deleted_or_moved(save : SaveResource) -> void:
+ _save_dictionary.erase(get_save_file_name(save.save_name, save.session_tag))
diff --git a/game/src/Game/Autoload/SoundManager.gd b/game/src/Game/Autoload/SoundManager.gd
new file mode 100644
index 0000000..c58ce1a
--- /dev/null
+++ b/game/src/Game/Autoload/SoundManager.gd
@@ -0,0 +1,42 @@
+extends Node
+
+# REQUIREMENTS:
+# * SS-68
+
+const _audio_directory_path : StringName = &"res://audio/sfx/"
+
+var _loaded_sound : Dictionary = {}
+
+var _bus_to_stream_player : Dictionary = {}
+
+# REQUIREMENTS:
+# * SND-10
+func _ready():
+ var dir = DirAccess.open(_audio_directory_path)
+ for fname in dir.get_files():
+ match fname.get_extension():
+ "ogg", "wav", "mp3":
+ _loaded_sound[fname.get_basename()] = load(_audio_directory_path.path_join(fname)) # SND-10
+
+func play_stream(sound : AudioStream, bus_type : String) -> void:
+ var player : AudioStreamPlayer = _bus_to_stream_player.get(bus_type)
+ if player == null:
+ player = AudioStreamPlayer.new()
+ player.bus = bus_type
+ player.stream = AudioStreamPolyphonic.new()
+ _bus_to_stream_player[bus_type] = player
+ add_child(player)
+ player.play()
+ var poly_playback : AudioStreamPlaybackPolyphonic = player.get_stream_playback()
+ poly_playback.play_stream(sound)
+
+func play(sound : String, bus_type : String) -> void:
+ play_stream(_loaded_sound[sound], bus_type)
+
+# REQUIREMENTS:
+# * SND-7
+func play_effect_stream(sound : AudioStream) -> void:
+ play_stream(sound, "SFX")
+
+func play_effect(sound : String) -> void:
+ play(sound, "SFX")