Skip to content

Implement in game menu for loading mods #512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions extension/doc_classes/GameSingleton.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@
Returns the localization key [String] of the mapmode with the specified [param index].
</description>
</method>
<method name="get_mod_info" qualifiers="const">
<return type="Dictionary[]" />
<description>
Returns an array of [Dictionary] containing information about available and loaded mods.
</description>
</method>
<method name="get_province_colour_texture" qualifiers="const">
<return type="ImageTexture" />
<description>
Expand Down Expand Up @@ -133,6 +139,8 @@
</method>
<method name="load_defines_compatibility_mode">
<return type="int" enum="Error" />
<param index="0" name="base_path" type="String" />
<param index="1" name="mods " type="PackedStringArray" />
<description>
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].
</description>
Expand All @@ -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.
</description>
</method>
<method name="set_compatibility_mode_roots">
<method name="set_compatibility_mode_base_path">
<return type="int" enum="Error" />
<param index="0" name="file_paths" type="PackedStringArray" />
<param index="1" name="replace_paths" type="PackedStringArray" default="PackedStringArray()" />
<param index="0" name="base_path" type="String" />
<description>
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].
</description>
</method>
<method name="set_mapmode">
Expand Down
85 changes: 66 additions & 19 deletions extension/src/openvic-extension/singletons/GameSingleton.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
#include "GameSingleton.hpp"

#include <cstdint>
#include <functional>
#include <string_view>

#include <godot_cpp/core/error_macros.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/string_name.hpp>
#include <godot_cpp/variant/typed_array.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

#include <openvic-simulation/dataloader/Dataloader.hpp>
#include <openvic-simulation/utility/Logger.hpp>

#include "openvic-extension/singletons/AssetManager.hpp"
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -108,6 +116,38 @@ void GameSingleton::setup_logger() {
});
}

TypedArray<Dictionary> 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<Dictionary> 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
Comment on lines +137 to +143
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use macros for this.

Copy link
Contributor

@wvpm wvpm Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just store it in a local variable.

When using methods to get a value, do not assume the method returns the exact same value each invocation.
Also the method might be costly.


results.push_back(std::move(mod_info_dictionary));
}

return results;
}

TypedArray<Dictionary> GameSingleton::get_bookmark_info() const {
static const StringName bookmark_info_name_key = "bookmark_name";
static const StringName bookmark_info_date_key = "bookmark_date";
Expand Down Expand Up @@ -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!");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the error macro?

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's best to return failed here. No point continuing with a broken state.
Also this can be done using ERR_FAIL_COND_V_MSG(!game_manager.load_mod_descriptors(), FAILED, "Failed to load mod descriptors!");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be better to load the base game if mods are broken or unavailable rather than crashing the whole game. Open to doing it either way, that was my thought process.

}

Dataloader::path_vector_t roots { Utilities::godot_to_std_string(base_path) }, replace_paths;
std::vector<std::string> 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as comment L667, better to return here and use error macro.

}

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you update the above 2 cases, please also update this for consistency.

Expand Down
10 changes: 6 additions & 4 deletions extension/src/openvic-extension/singletons/GameSingleton.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#include <godot_cpp/classes/image_texture.hpp>
#include <godot_cpp/classes/texture2d_array.hpp>
#include <godot_cpp/classes/control.hpp>
#include <godot_cpp/classes/global_constants.hpp>
#include <godot_cpp/variant/dictionary.hpp>

#include <openvic-simulation/GameManager.hpp>
#include <openvic-simulation/dataloader/Dataloader.hpp>
Expand Down Expand Up @@ -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<godot::Dictionary> get_bookmark_info() const;
godot::TypedArray<godot::Dictionary> 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);
Expand Down
22 changes: 10 additions & 12 deletions game/src/Systems/Startup/GameStart.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions game/src/UI/GameMenu/GameMenu/GameMenu.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
12 changes: 10 additions & 2 deletions game/src/UI/GameMenu/GameMenu/GameMenu.tscn
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -53,10 +59,12 @@ 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"]
[connection signal="back_button_pressed" from="OptionsMenu" to="." method="_on_options_menu_back_button_pressed"]
[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"]
14 changes: 7 additions & 7 deletions game/src/UI/GameMenu/MainMenu/MainMenu.gd
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Loading
Loading