diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation
index 053ba2de..3768b5e6 160000
--- a/extension/deps/openvic-simulation
+++ b/extension/deps/openvic-simulation
@@ -1 +1 @@
-Subproject commit 053ba2de4ff39ca3fc90a809d80aad180cf87b5a
+Subproject commit 3768b5e62ea92e71d0df7b68e304aff5fd282a3e
diff --git a/extension/doc_classes/GameSingleton.xml b/extension/doc_classes/GameSingleton.xml
index ab927635..d1eb0bc4 100644
--- a/extension/doc_classes/GameSingleton.xml
+++ b/extension/doc_classes/GameSingleton.xml
@@ -84,6 +84,12 @@
Returns the localization key [String] of the mapmode with the specified [param index].
+
+
+
+ Returns an array of [Dictionary] containing information about available and loaded mods.
+
+
@@ -133,6 +139,8 @@
+
+
Load compatibility mode text defines, localization string and map and flag images. Returns [code]FAILED[/code] if there are any problems when loading all this data, otherwise returns [code]OK[/code].
@@ -151,12 +159,11 @@
Searches for the base game's install path, checking the [param hint_path] if it's provided as well as the Steam install folder as identified by the [code]"libraryfolders.vdf"[/code] file. This function will return an empty [String] should it fail to find the base game's install path.
-
+
-
-
+
- Set the dataloading roots to those provided in [param file_paths], ignoring the filepaths in [param replace_paths] in favor of mods, which should contain full filepaths to the base game's installation and to any mods that are to be loaded on top of it. Returns [code]FAILED[/code] if there are any problems when setting the dataloading roots, otherwise returns [code]OK[/code].
+ Sets the root dataloader path to [param base_path]. Returns [code]FAILED[/code] if the path is invalid, otherwise returns [code]OK[/code].
diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp
index 18abb2f0..b7554550 100644
--- a/extension/src/openvic-extension/singletons/GameSingleton.cpp
+++ b/extension/src/openvic-extension/singletons/GameSingleton.cpp
@@ -1,10 +1,17 @@
#include "GameSingleton.hpp"
+#include
#include
+#include
#include
+#include
+#include
+#include
+#include
#include
+#include
#include
#include "openvic-extension/singletons/AssetManager.hpp"
@@ -34,13 +41,14 @@ StringName const& GameSingleton::_signal_mapmode_changed() {
void GameSingleton::_bind_methods() {
OV_BIND_SMETHOD(setup_logger);
- OV_BIND_METHOD(GameSingleton::load_defines_compatibility_mode);
- OV_BIND_METHOD(GameSingleton::set_compatibility_mode_roots, { "file_paths", "replace_paths" }, DEFVAL(PackedStringArray{}));
+ OV_BIND_METHOD(GameSingleton::set_compatibility_mode_base_path, { "base_path" });
+ OV_BIND_METHOD(GameSingleton::load_defines_compatibility_mode, { "base_path", "mods " });
OV_BIND_SMETHOD(search_for_game_path, { "hint_path" }, DEFVAL(String {}));
OV_BIND_METHOD(GameSingleton::lookup_file_path, { "path" });
OV_BIND_METHOD(GameSingleton::get_bookmark_info);
+ OV_BIND_METHOD(GameSingleton::get_mod_info);
OV_BIND_METHOD(GameSingleton::setup_game, { "bookmark_index" });
OV_BIND_METHOD(GameSingleton::start_game_session);
@@ -108,6 +116,38 @@ void GameSingleton::setup_logger() {
});
}
+TypedArray GameSingleton::get_mod_info() const {
+ static const StringName mod_info_identifier_key = "mod_identifier";
+ static const StringName mod_info_dependencies_key = "mod_dependencies";
+ static const StringName mod_info_loaded_key = "mod_loaded";
+
+ TypedArray results;
+
+ for (Mod const& mod : game_manager.get_mod_manager().get_mods()) {
+ Dictionary mod_info_dictionary;
+
+ mod_info_dictionary[mod_info_identifier_key] = Utilities::std_to_godot_string(mod.get_identifier());
+
+ PackedStringArray dependencies;
+ for (std::string_view dep_id : mod.get_dependencies()) {
+ dependencies.push_back(Utilities::std_to_godot_string(dep_id));
+ }
+ mod_info_dictionary[mod_info_dependencies_key] = std::move(dependencies);
+
+ #define loaded_mods game_manager.get_mod_manager().get_loaded_mods()
+ if (std::find(loaded_mods.begin(), loaded_mods.end(), &mod) != loaded_mods.end()) {
+ mod_info_dictionary[mod_info_loaded_key] = true;
+ } else {
+ mod_info_dictionary[mod_info_loaded_key] = false;
+ }
+ #undef loaded_mods
+
+ results.push_back(std::move(mod_info_dictionary));
+ }
+
+ return results;
+}
+
TypedArray GameSingleton::get_bookmark_info() const {
static const StringName bookmark_info_name_key = "bookmark_name";
static const StringName bookmark_info_date_key = "bookmark_date";
@@ -610,29 +650,36 @@ Error GameSingleton::_load_flag_sheet() {
return ret;
}
-Error GameSingleton::set_compatibility_mode_roots(
- PackedStringArray const& file_paths, godot::PackedStringArray const& replace_paths
-) {
- Dataloader::path_vector_t roots;
- roots.reserve(file_paths.size());
- for (String const& path : file_paths) {
- roots.emplace_back(Utilities::godot_to_std_string(path));
- }
-
- Dataloader::path_vector_t replace;
- replace.reserve(replace_paths.size());
- for (String const& path : replace_paths) {
- replace.emplace_back(Utilities::godot_to_std_string(path));
+Error GameSingleton::set_compatibility_mode_base_path(String const& base_path) {
+ Dataloader::path_vector_t roots { Utilities::godot_to_std_string(base_path) }, replace_paths;
+ if (!game_manager.set_base_path(roots)) {
+ UtilityFunctions::push_error("Failed to set base path!");
+ return FAILED;
}
-
- ERR_FAIL_COND_V_MSG(!game_manager.set_roots(roots, replace), FAILED, "Failed to set dataloader roots!");
return OK;
}
-Error GameSingleton::load_defines_compatibility_mode() {
+Error GameSingleton::load_defines_compatibility_mode(String const& base_path, PackedStringArray const& mods) {
Error err = OK;
- auto add_message = std::bind_front(&LoadLocalisation::add_message, LoadLocalisation::get_singleton());
+ if (!game_manager.load_mod_descriptors()) {
+ UtilityFunctions::push_error("Failed to load mod descriptors!");
+ err = FAILED;
+ }
+
+ Dataloader::path_vector_t roots { Utilities::godot_to_std_string(base_path) }, replace_paths;
+ std::vector converted_mods;
+ converted_mods.reserve(mods.size());
+ for (String const& mod : mods) {
+ converted_mods.push_back(Utilities::godot_to_std_string(mod));
+ }
+
+ if (!game_manager.load_mods(roots, replace_paths, converted_mods)) {
+ UtilityFunctions::push_error("Failed to load mods!");
+ err = FAILED;
+ }
+
+ auto add_message = std::bind_front(&LoadLocalisation::add_message, LoadLocalisation::get_singleton());
if (!game_manager.load_definitions(add_message)) {
UtilityFunctions::push_error("Failed to load defines!");
err = FAILED;
diff --git a/extension/src/openvic-extension/singletons/GameSingleton.hpp b/extension/src/openvic-extension/singletons/GameSingleton.hpp
index 613458cc..1fddeb4c 100644
--- a/extension/src/openvic-extension/singletons/GameSingleton.hpp
+++ b/extension/src/openvic-extension/singletons/GameSingleton.hpp
@@ -2,6 +2,9 @@
#include
#include
+#include
+#include
+#include
#include
#include
@@ -70,15 +73,14 @@ namespace OpenVic {
/* Load the game's defines in compatibility mode from the filepath
* pointing to the defines folder. */
- godot::Error set_compatibility_mode_roots(
- godot::PackedStringArray const& file_paths, godot::PackedStringArray const& replace_paths = {}
- );
- godot::Error load_defines_compatibility_mode();
+ godot::Error set_compatibility_mode_base_path(godot::String const& base_path);
+ godot::Error load_defines_compatibility_mode(godot::String const& base_path, godot::PackedStringArray const& mods);
static godot::String search_for_game_path(godot::String const& hint_path = {});
godot::String lookup_file_path(godot::String const& path) const;
godot::TypedArray get_bookmark_info() const;
+ godot::TypedArray get_mod_info() const;
/* Post-load/restart game setup - reset the game to post-load state and load the specified bookmark. */
godot::Error setup_game(int32_t bookmark_index);
diff --git a/game/src/Systems/Startup/GameStart.gd b/game/src/Systems/Startup/GameStart.gd
index 8be951cd..e8272756 100644
--- a/game/src/Systems/Startup/GameStart.gd
+++ b/game/src/Systems/Startup/GameStart.gd
@@ -13,7 +13,8 @@ const GameMenuScene := preload("res://src/UI/GameMenu/GameMenu/GameMenu.tscn")
@export var setting_name : String = "base_defines_path"
var _settings_base_path : String = ""
-var _compatibility_path_list : PackedStringArray = []
+var actual_base_path : String = ""
+var mod_names : PackedStringArray = []
func _enter_tree() -> void:
Keychain.keep_binding_check = func(action_name : StringName) -> bool:
@@ -77,13 +78,9 @@ func _save_setting(file : ConfigFile) -> void:
file.set_value(section_name, setting_name, _settings_base_path)
func _setup_compatibility_mode_paths() -> void:
- # To test mods, set your base path to Victoria II and then pass mods in reverse order with --mod="mod" for each mod.
-
var arg_base_path : String = ArgumentParser.get_argument(&"base-path", "")
var arg_search_path : String = ArgumentParser.get_argument(&"search-path", "")
- var actual_base_path : String = ""
-
if arg_base_path:
if arg_search_path:
push_warning("Exact base path and search base path arguments both used:\nBase: ", arg_base_path, "\nSearch: ", arg_search_path)
@@ -127,21 +124,22 @@ func _setup_compatibility_mode_paths() -> void:
# Save the path found in the search
Events.Options.save_settings_to_file()
- _compatibility_path_list = [actual_base_path]
-
# Add mod paths
- var settings_mod_names : PackedStringArray = ArgumentParser.get_argument(&"mod", "")
- for mod_name : String in settings_mod_names:
- _compatibility_path_list.push_back(actual_base_path + "/mod/" + mod_name)
+ var mod_status_file := ConfigFile.new()
+ mod_status_file.load("user://mods.cfg")
+ mod_names = mod_status_file.get_value("mods", "load_list", [])
+ for mod in ArgumentParser.get_argument(&"mod", ""):
+ if mod not in mod_names and mod != "":
+ mod_names.push_back(mod)
func _load_compatibility_mode() -> void:
- if GameSingleton.set_compatibility_mode_roots(_compatibility_path_list) != OK:
+ if GameSingleton.set_compatibility_mode_base_path(actual_base_path) != OK:
push_error("Errors setting game roots!")
CursorManager.initial_cursor_setup()
setup_title_theme()
- if GameSingleton.load_defines_compatibility_mode() != OK:
+ if GameSingleton.load_defines_compatibility_mode(actual_base_path, mod_names) != OK:
push_error("Errors loading game defines!")
SoundSingleton.load_sounds()
diff --git a/game/src/UI/GameMenu/GameMenu/GameMenu.gd b/game/src/UI/GameMenu/GameMenu/GameMenu.gd
index cbb60e53..4bebb6ff 100644
--- a/game/src/UI/GameMenu/GameMenu/GameMenu.gd
+++ b/game/src/UI/GameMenu/GameMenu/GameMenu.gd
@@ -5,6 +5,7 @@ extends Control
@export var _multiplayer_menu : Control
@export var _lobby_menu : Control
@export var _credits_menu : Control
+@export var _mod_menu : Control
# REQUIREMENTS
# * SS-10
@@ -51,3 +52,13 @@ func _on_multiplayer_menu_back_button_pressed() -> void:
func _on_main_menu_multiplayer_button_pressed() -> void:
_multiplayer_menu.show()
_main_menu.hide()
+
+
+func _on_main_menu_mod_button_pressed() -> void:
+ _mod_menu.show()
+ _main_menu.hide()
+
+
+func _on_mod_menu_back_button_pressed() -> void:
+ _mod_menu.hide()
+ _main_menu.show()
diff --git a/game/src/UI/GameMenu/GameMenu/GameMenu.tscn b/game/src/UI/GameMenu/GameMenu/GameMenu.tscn
index c9403720..726315f7 100644
--- a/game/src/UI/GameMenu/GameMenu/GameMenu.tscn
+++ b/game/src/UI/GameMenu/GameMenu/GameMenu.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=8 format=3 uid="uid://o4u142w4qkln"]
+[gd_scene load_steps=9 format=3 uid="uid://o4u142w4qkln"]
[ext_resource type="Script" uid="uid://bf36b41ip0jyu" path="res://src/UI/GameMenu/GameMenu/GameMenu.gd" id="1_cafwe"]
[ext_resource type="PackedScene" uid="uid://bp5n3mlu45ygw" path="res://src/UI/GameMenu/MainMenu/MainMenu.tscn" id="2_2jbkh"]
@@ -7,8 +7,9 @@
[ext_resource type="PackedScene" uid="uid://do60kx0d3nrh4" path="res://src/UI/GameMenu/LobbyMenu/LobbyMenu.tscn" id="4_nofk1"]
[ext_resource type="PackedScene" uid="uid://btri1i0hkhdsh" path="res://src/UI/GameMenu/MultiplayerMenu/MultiplayerMenu.tscn" id="4_s7nkl"]
[ext_resource type="PackedScene" uid="uid://cvl76duuym1wq" path="res://src/UI/Shared/MusicMenu/MusicMenu.tscn" id="6_lts1m"]
+[ext_resource type="PackedScene" uid="uid://bh7otkxuf17sg" path="res://src/UI/GameMenu/ModMenu/ModMenu.tscn" id="7_mu3ov"]
-[node name="GameMenu" type="Control" node_paths=PackedStringArray("_main_menu", "_options_menu", "_multiplayer_menu", "_lobby_menu", "_credits_menu")]
+[node name="GameMenu" type="Control" node_paths=PackedStringArray("_main_menu", "_options_menu", "_multiplayer_menu", "_lobby_menu", "_credits_menu", "_mod_menu")]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@@ -22,6 +23,7 @@ _options_menu = NodePath("OptionsMenu")
_multiplayer_menu = NodePath("MultiplayerMenu")
_lobby_menu = NodePath("LobbyMenu")
_credits_menu = NodePath("CreditsMenu")
+_mod_menu = NodePath("ModMenu")
[node name="MainMenu" parent="." instance=ExtResource("2_2jbkh")]
layout_mode = 1
@@ -43,6 +45,10 @@ layout_mode = 1
visible = false
layout_mode = 1
+[node name="ModMenu" parent="." instance=ExtResource("7_mu3ov")]
+visible = false
+layout_mode = 1
+
[node name="MusicPlayer" parent="." instance=ExtResource("6_lts1m")]
layout_mode = 1
anchors_preset = 1
@@ -53,6 +59,7 @@ offset_right = -34.0
grow_horizontal = 0
[connection signal="credits_button_pressed" from="MainMenu" to="." method="_on_main_menu_credits_button_pressed"]
+[connection signal="mod_button_pressed" from="MainMenu" to="." method="_on_main_menu_mod_button_pressed"]
[connection signal="multiplayer_button_pressed" from="MainMenu" to="." method="_on_main_menu_multiplayer_button_pressed"]
[connection signal="new_game_button_pressed" from="MainMenu" to="." method="_on_main_menu_new_game_button_pressed"]
[connection signal="options_button_pressed" from="MainMenu" to="." method="_on_main_menu_options_button_pressed"]
@@ -60,3 +67,4 @@ grow_horizontal = 0
[connection signal="back_button_pressed" from="MultiplayerMenu" to="." method="_on_multiplayer_menu_back_button_pressed"]
[connection signal="back_button_pressed" from="LobbyMenu" to="." method="_on_lobby_menu_back_button_pressed"]
[connection signal="back_button_pressed" from="CreditsMenu" to="." method="_on_credits_back_button_pressed"]
+[connection signal="back_button_pressed" from="ModMenu" to="." method="_on_mod_menu_back_button_pressed"]
diff --git a/game/src/UI/GameMenu/MainMenu/MainMenu.gd b/game/src/UI/GameMenu/MainMenu/MainMenu.gd
index b5983bc9..b8047086 100644
--- a/game/src/UI/GameMenu/MainMenu/MainMenu.gd
+++ b/game/src/UI/GameMenu/MainMenu/MainMenu.gd
@@ -1,10 +1,11 @@
extends Control
-signal options_button_pressed
signal new_game_button_pressed
-signal credits_button_pressed
-signal multiplayer_button_pressed
signal continue_button_pressed
+signal multiplayer_button_pressed
+signal mod_button_pressed
+signal options_button_pressed
+signal credits_button_pressed
@export
var _new_game_button : BaseButton
@@ -18,19 +19,18 @@ func _ready() -> void:
# * SS-14
# * UIFUN-32
func _on_new_game_button_pressed() -> void:
- print("Start a new game!")
new_game_button_pressed.emit()
func _on_continue_button_pressed() -> void:
- print("Continue last game!")
continue_button_pressed.emit()
func _on_multi_player_button_pressed() -> void:
- print("Have fun with friends!")
multiplayer_button_pressed.emit()
+func _on_mod_button_pressed():
+ mod_button_pressed.emit()
+
func _on_options_button_pressed() -> void:
- print("Check out some options!")
options_button_pressed.emit()
# REQUIREMENTS
diff --git a/game/src/UI/GameMenu/MainMenu/MainMenu.tscn b/game/src/UI/GameMenu/MainMenu/MainMenu.tscn
index 2168e273..84798e9d 100644
--- a/game/src/UI/GameMenu/MainMenu/MainMenu.tscn
+++ b/game/src/UI/GameMenu/MainMenu/MainMenu.tscn
@@ -54,11 +54,10 @@ alignment = 1
editor_description = "UI-26"
layout_mode = 2
size_flags_horizontal = 3
-focus_neighbor_left = NodePath("../ExitButton")
-focus_neighbor_top = NodePath("../ExitButton")
+focus_neighbor_left = NodePath("../../../BottomMargin/HBoxContainer/LocaleButton")
focus_neighbor_right = NodePath("../ContinueButton")
focus_next = NodePath("../ContinueButton")
-focus_previous = NodePath("../ExitButton")
+focus_previous = NodePath("../../../BottomMargin/HBoxContainer/LocaleButton")
theme_type_variation = &"TitleButton"
text = "MAINMENU_NEW_GAME"
clip_text = true
@@ -80,35 +79,34 @@ editor_description = "UI-27"
layout_mode = 2
size_flags_horizontal = 3
focus_neighbor_left = NodePath("../ContinueButton")
-focus_neighbor_right = NodePath("../OptionsButton")
-focus_next = NodePath("../OptionsButton")
+focus_neighbor_right = NodePath("../ModButton")
+focus_next = NodePath("../ModButton")
focus_previous = NodePath("../ContinueButton")
theme_type_variation = &"TitleButton"
text = "MAINMENU_MULTIPLAYER"
clip_text = true
-[node name="OptionsButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
-editor_description = "UI-5"
+[node name="ModButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
layout_mode = 2
size_flags_horizontal = 3
focus_neighbor_left = NodePath("../MultiplayerButton")
-focus_neighbor_right = NodePath("../CreditsButton")
-focus_next = NodePath("../CreditsButton")
+focus_neighbor_right = NodePath("../OptionsButton")
+focus_next = NodePath("../OptionsButton")
focus_previous = NodePath("../MultiplayerButton")
theme_type_variation = &"TitleButton"
-text = "MAINMENU_OPTIONS"
+text = "MAINMENU_MODS"
clip_text = true
-[node name="CreditsButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
-editor_description = "UI-32"
+[node name="OptionsButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
+editor_description = "UI-5"
layout_mode = 2
size_flags_horizontal = 3
-focus_neighbor_left = NodePath("../OptionsButton")
+focus_neighbor_left = NodePath("../ModButton")
focus_neighbor_right = NodePath("../ExitButton")
focus_next = NodePath("../ExitButton")
-focus_previous = NodePath("../OptionsButton")
+focus_previous = NodePath("../ModButton")
theme_type_variation = &"TitleButton"
-text = "MAINMENU_CREDITS"
+text = "MAINMENU_OPTIONS"
clip_text = true
[node name="ExitButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
@@ -116,8 +114,8 @@ editor_description = "UI-3"
layout_mode = 2
size_flags_horizontal = 3
focus_neighbor_left = NodePath("../OptionsButton")
-focus_neighbor_right = NodePath("../NewGameButton")
-focus_next = NodePath("../NewGameButton")
+focus_neighbor_right = NodePath("../../../BottomMargin/HBoxContainer/CreditsButton")
+focus_next = NodePath("../../../BottomMargin/HBoxContainer/CreditsButton")
focus_previous = NodePath("../OptionsButton")
theme_type_variation = &"TitleButton"
text = "MAINMENU_EXIT"
@@ -132,13 +130,37 @@ size_flags_stretch_ratio = 0.35
layout_mode = 2
theme_type_variation = &"BottomMargin"
-[node name="ReleaseInfoBox" parent="MenuPanel/MenuList/BottomMargin" instance=ExtResource("3_km0er")]
+[node name="HBoxContainer" type="HBoxContainer" parent="MenuPanel/MenuList/BottomMargin"]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="ReleaseInfoBox" parent="MenuPanel/MenuList/BottomMargin/HBoxContainer" instance=ExtResource("3_km0er")]
+layout_mode = 2
+
+[node name="Spacer" type="Control" parent="MenuPanel/MenuList/BottomMargin/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="CreditsButton" type="Button" parent="MenuPanel/MenuList/BottomMargin/HBoxContainer"]
+editor_description = "UI-32"
+custom_minimum_size = Vector2(160, 0)
layout_mode = 2
+focus_neighbor_left = NodePath("../../../ButtonListMargin/ButtonList/ExitButton")
+focus_neighbor_right = NodePath("../LocaleButton")
+focus_next = NodePath("../LocaleButton")
+focus_previous = NodePath("../../../ButtonListMargin/ButtonList/ExitButton")
+theme_type_variation = &"TitleButton"
+text = "MAINMENU_CREDITS"
+clip_text = true
-[node name="LocaleButton" parent="MenuPanel/MenuList/BottomMargin" instance=ExtResource("3_amonp")]
+[node name="LocaleButton" parent="MenuPanel/MenuList/BottomMargin/HBoxContainer" instance=ExtResource("3_amonp")]
editor_description = "SS-87"
layout_mode = 2
size_flags_horizontal = 8
+focus_neighbor_left = NodePath("../CreditsButton")
+focus_neighbor_right = NodePath("../../../ButtonListMargin/ButtonList/NewGameButton")
+focus_next = NodePath("../../../ButtonListMargin/ButtonList/NewGameButton")
+focus_previous = NodePath("../CreditsButton")
alignment = 0
text_overrun_behavior = 4
@@ -146,6 +168,7 @@ text_overrun_behavior = 4
[connection signal="visibility_changed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/NewGameButton" to="." method="_on_new_game_button_visibility_changed"]
[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/ContinueButton" to="." method="_on_continue_button_pressed"]
[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/MultiplayerButton" to="." method="_on_multi_player_button_pressed"]
+[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/ModButton" to="." method="_on_mod_button_pressed"]
[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/OptionsButton" to="." method="_on_options_button_pressed"]
-[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/CreditsButton" to="." method="_on_credits_button_pressed"]
[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/ExitButton" to="." method="_on_exit_button_pressed"]
+[connection signal="pressed" from="MenuPanel/MenuList/BottomMargin/HBoxContainer/CreditsButton" to="." method="_on_credits_button_pressed"]
diff --git a/game/src/UI/GameMenu/ModMenu/ModMenu.gd b/game/src/UI/GameMenu/ModMenu/ModMenu.gd
new file mode 100644
index 00000000..74f86dc5
--- /dev/null
+++ b/game/src/UI/GameMenu/ModMenu/ModMenu.gd
@@ -0,0 +1,96 @@
+extends Control
+
+signal back_button_pressed
+
+@export var mod_list_box : VBoxContainer
+
+var mod_info: Array[Dictionary]
+var checkboxes = {}
+var selected_mods : PackedStringArray = []
+var selected_and_required_mods : PackedStringArray = []
+
+func _ready():
+ mod_info = GameSingleton.get_mod_info()
+ var mod_status_file := ConfigFile.new()
+ mod_status_file.load("user://mods.cfg")
+ selected_mods = mod_status_file.get_value("mods", "load_list", [])
+ for mod in selected_mods:
+ _select_mod_dependencies(mod)
+
+ for mod in mod_info:
+ var mod_name : String = mod["mod_identifier"]
+ var mod_loaded : bool = mod["mod_loaded"]
+
+ var hbox : HBoxContainer = HBoxContainer.new()
+ hbox.name = mod_name
+
+ var checkbox : CheckBox = CheckBox.new()
+ checkbox.text = mod_name
+ checkbox.button_pressed = mod_loaded
+ checkbox.disabled = mod_loaded and mod_name not in selected_mods
+ checkbox.toggled.connect(_on_mod_toggled.bind(mod_name))
+ checkboxes[mod_name] = checkbox
+ hbox.add_child(checkbox)
+
+ var status : Label = Label.new()
+ status.text = "Loaded" if mod_loaded else ""
+ hbox.add_child(status)
+
+ mod_list_box.add_child(hbox)
+
+func _select_mod_dependencies(mod_name: String):
+ if mod_name not in selected_and_required_mods:
+ selected_and_required_mods.push_back(mod_name)
+ for dep in _get_mod_from_identifier(mod_name)["mod_dependencies"]:
+ if checkboxes.has(dep):
+ var dep_checkbox: CheckBox = checkboxes[dep]
+ dep_checkbox.set_pressed_no_signal(true)
+ dep_checkbox.disabled = true
+ dep_checkbox.tooltip_text = "This mod is a dependency of another mod, and cannot be disabled."
+ _select_mod_dependencies(dep)
+
+func _deselect_mod_dependencies(mod_name: String):
+ if not _is_dependency_required(mod_name):
+ if mod_name in selected_and_required_mods:
+ selected_and_required_mods.remove_at(selected_and_required_mods.find(mod_name))
+ for dep in _get_mod_from_identifier(mod_name)["mod_dependencies"]:
+ if checkboxes.has(dep):
+ var dep_checkbox: CheckBox = checkboxes[dep]
+ dep_checkbox.set_pressed_no_signal(false)
+ dep_checkbox.disabled = false
+ dep_checkbox.tooltip_text = ""
+ _deselect_mod_dependencies(dep)
+
+func _get_mod_from_identifier(mod_name: String) -> Dictionary:
+ for mod in mod_info:
+ if mod["mod_identifier"] == mod_name:
+ return mod
+ return {"mod_dependencies":[]}
+
+func _is_dependency_required(dep_name: String) -> bool:
+ for mod_name in selected_mods:
+ var deps: PackedStringArray = _get_mod_from_identifier(mod_name)["mod_dependencies"]
+ if dep_name in deps:
+ return true
+ return false
+
+func _on_mod_toggled(checked: bool, mod_name: String) -> void:
+ if checked:
+ print("Selected Mod: " + mod_name)
+ selected_mods.push_back(mod_name)
+ _select_mod_dependencies(mod_name)
+ else:
+ print("Unselected Mod: " + mod_name)
+ selected_mods.remove_at(selected_mods.find(mod_name))
+ _deselect_mod_dependencies(mod_name)
+
+func _on_save_button_pressed():
+ var mod_status_file := ConfigFile.new()
+ mod_status_file.set_value("mods", "load_list", selected_mods)
+ mod_status_file.save("user://mods.cfg")
+ # reload game to apply changes
+ OS.set_restart_on_exit(true)
+ get_tree().quit()
+
+func _on_back_button_pressed():
+ back_button_pressed.emit()
diff --git a/game/src/UI/GameMenu/ModMenu/ModMenu.gd.uid b/game/src/UI/GameMenu/ModMenu/ModMenu.gd.uid
new file mode 100644
index 00000000..aa880dd6
--- /dev/null
+++ b/game/src/UI/GameMenu/ModMenu/ModMenu.gd.uid
@@ -0,0 +1 @@
+uid://daf0wi7bbaf3n
diff --git a/game/src/UI/GameMenu/ModMenu/ModMenu.tscn b/game/src/UI/GameMenu/ModMenu/ModMenu.tscn
new file mode 100644
index 00000000..66edf2aa
--- /dev/null
+++ b/game/src/UI/GameMenu/ModMenu/ModMenu.tscn
@@ -0,0 +1,69 @@
+[gd_scene load_steps=3 format=3 uid="uid://bh7otkxuf17sg"]
+
+[ext_resource type="Theme" uid="uid://fbxssqcg1s0m" path="res://assets/graphics/theme/options_menu.tres" id="1_8in6e"]
+[ext_resource type="Script" uid="uid://daf0wi7bbaf3n" path="res://src/UI/GameMenu/ModMenu/ModMenu.gd" id="1_ahk3j"]
+
+[node name="ModMenu" type="PanelContainer" node_paths=PackedStringArray("mod_list_box")]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_8in6e")
+theme_type_variation = &"BackgroundPanel"
+script = ExtResource("1_ahk3j")
+mod_list_box = NodePath("Margin/PanelContainer/Margin/VBox/ModListBox")
+
+[node name="Margin" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 256
+theme_override_constants/margin_top = 180
+theme_override_constants/margin_right = 256
+theme_override_constants/margin_bottom = 180
+
+[node name="PanelContainer" type="PanelContainer" parent="Margin"]
+layout_mode = 2
+
+[node name="Margin" type="MarginContainer" parent="Margin/PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 12
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 12
+theme_override_constants/margin_bottom = 10
+
+[node name="VBox" type="VBoxContainer" parent="Margin/PanelContainer/Margin"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Margin/PanelContainer/Margin/VBox"]
+layout_mode = 2
+text = "Mod Menu"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="ModListBox" type="VBoxContainer" parent="Margin/PanelContainer/Margin/VBox"]
+layout_mode = 2
+
+[node name="Spacer" type="Control" parent="Margin/PanelContainer/Margin/VBox"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="HBox" type="HBoxContainer" parent="Margin/PanelContainer/Margin/VBox"]
+layout_mode = 2
+alignment = 1
+
+[node name="SaveButton" type="Button" parent="Margin/PanelContainer/Margin/VBox/HBox"]
+layout_mode = 2
+size_flags_horizontal = 4
+text = " Save (Reloads Game)"
+
+[node name="Spacer" type="Control" parent="Margin/PanelContainer/Margin/VBox/HBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="BackButton" type="Button" parent="Margin/PanelContainer/Margin/VBox/HBox"]
+layout_mode = 2
+size_flags_horizontal = 4
+text = " Back "
+
+[connection signal="pressed" from="Margin/PanelContainer/Margin/VBox/HBox/SaveButton" to="." method="_on_save_button_pressed"]
+[connection signal="pressed" from="Margin/PanelContainer/Margin/VBox/HBox/BackButton" to="." method="_on_back_button_pressed"]