Skip to content

Commit 6119166

Browse files
KANAjetztQubus0
andauthored
feat: ✨ options for game version validation (#536)
* feat: ✨ option to disable game version validation * feat: ✨ added custom validation option * docs: 📝 added missing doc comments * refactor: ♻️ use ENUM * refactor: ♻️ only pass `ml_options` * refactor: ♻️ move `customize_script_path` out of export group * docs: 📝 added example customize script * style: ✏️ improved spelling * docs: 📝 reworked comments * refactor: ♻️ `ml_options_path` as param for easier testing * refactor: 🔥 remove example script moved to docs page * refactor: ♻️ removed example added `@tutorial` * test: 🧪 added custom validation test * fix: 🧪 fixed test setup * fix: 🧪 removed editor override * fix: 🐛 set `customize_script_path` outside of for loop * refactor: 🚚 added sub dir * test: 🧪 added test for game version validation disabled * fix: 🧪 updated custom script path * test: 🧪 added `test_game_verion_validation_default` * fix: 🧪 replace white space chars with `""` * refactor: ♻️ clean up a bit * test: 🧪 added no callable set test * Update addons/mod_loader/resources/options_profile.gd Co-authored-by: steen <steen.rickmer@gmx.de> * Update addons/mod_loader/resources/options_profile.gd --------- Co-authored-by: steen <steen.rickmer@gmx.de>
1 parent a0b3872 commit 6119166

14 files changed

+306
-8
lines changed

addons/mod_loader/mod_loader_store.gd

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,10 @@ func _exit_tree() -> void:
129129

130130

131131
# Update ModLoader's options, via the custom options resource
132-
func _update_ml_options_from_options_resource() -> void:
133-
# Path to the options resource
134-
# See: res://addons/mod_loader/resources/options_current.gd
135-
var ml_options_path := "res://addons/mod_loader/options/options.tres"
136-
132+
#
133+
# Parameters:
134+
# - ml_options_path: Path to the options resource. See: res://addons/mod_loader/resources/options_current.gd
135+
func _update_ml_options_from_options_resource(ml_options_path := "res://addons/mod_loader/options/options.tres") -> void:
137136
# Get user options for ModLoader
138137
if not _ModLoaderFile.file_exists(ml_options_path) and not ResourceLoader.exists(ml_options_path):
139138
ModLoaderLog.fatal(str("A critical file is missing: ", ml_options_path), LOG_NAME)
@@ -183,6 +182,9 @@ func _update_ml_options_from_options_resource() -> void:
183182
# Update from the options in the resource
184183
ml_options = override_options
185184

185+
if not ml_options.customize_script_path.is_empty():
186+
ml_options.customize_script_instance = load(ml_options.customize_script_path).new(ml_options)
187+
186188

187189
# Update ModLoader's options, via CLI args
188190
func _update_ml_options_from_cli_args() -> void:

addons/mod_loader/resources/mod_manifest.gd

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ func validate(manifest: Dictionary, path: String) -> bool:
113113
config_schema = ModLoaderUtils.get_dict_from_dict(godot_details, "config_schema")
114114
steam_workshop_id = ModLoaderUtils.get_string_from_dict(godot_details, "steam_workshop_id")
115115

116-
_is_game_version_compatible(mod_id)
116+
if ModLoaderStore.ml_options.game_version_validation == ModLoaderOptionsProfile.VERSION_VALIDATION.DEFAULT:
117+
_is_game_version_compatible(mod_id)
118+
119+
if ModLoaderStore.ml_options.game_version_validation == ModLoaderOptionsProfile.VERSION_VALIDATION.CUSTOM:
120+
if ModLoaderStore.ml_options.custom_game_version_validation_callable:
121+
ModLoaderStore.ml_options.custom_game_version_validation_callable.call(self)
122+
else:
123+
ModLoaderLog.error("No custom game version validation callable detected. Please provide a valid validation callable.", LOG_NAME)
117124

118125
is_mod_id_array_valid(mod_id, dependencies, "dependency")
119126
is_mod_id_array_valid(mod_id, incompatibilities, "incompatibility")

addons/mod_loader/resources/options_profile.gd

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,52 @@
11
class_name ModLoaderOptionsProfile
22
extends Resource
3+
##
4+
## Class to define and store Mod Loader Options.
5+
##
6+
## @tutorial(Example Customization Script): https://wiki.godotmodding.com/guides/integration/mod_loader_options/#game-version-validation
37

48

9+
## Settings for game version validation.
10+
enum VERSION_VALIDATION {
11+
## Uses the default semantic versioning (semver) validation.
12+
DEFAULT,
13+
14+
## Disables validation of the game version specified in [member semantic_version]
15+
## and the mod's [member ModManifest.compatible_game_version].
16+
DISABLED,
17+
18+
## Enables custom game version validation.
19+
## Use [member customize_script_path] to specify a script that customizes the Mod Loader options.
20+
## In this script, you must set [member custom_game_version_validation_callable]
21+
## to a custom validation [Callable].
22+
## [br]
23+
## ===[br]
24+
## [b]Note:[color=note "Easier Mod Loader Updates"][/color][/b][br]
25+
## Using a custom script allows you to keep your code outside the addons directory,
26+
## making it easier to update the mod loader without affecting your modifications. [br]
27+
## ===[br]
28+
CUSTOM,
29+
}
30+
531
## Can be used to disable mods for specific plaforms by using feature overrides
632
@export var enable_mods: bool = true
733
## List of mod ids that can't be turned on or off
834
@export var locked_mods: Array[String] = []
9-
35+
## List of mods that will not be loaded
1036
@export var disabled_mods: Array[String] = []
1137
## Disables the requirement for the mod loader autoloads to be first
1238
@export var allow_modloader_autoloads_anywhere: bool = false
13-
39+
## This script is loaded after [member ModLoaderStore.ml_options] has been initialized.
40+
## It is instantiated with [member ModLoaderStore.ml_options] as an argument.
41+
## Use this script to apply settings that cannot be configured through the editor UI.
42+
##
43+
## For an example, see [enum VERSION_VALIDATION] [code]CUSTOM[/code] or
44+
## [code]res://addons/mod_loader/options/example_customize_script.gd[/code].
45+
@export_file var customize_script_path: String
1446

1547
@export_group("Logging")
48+
## Sets the logging verbosity level.
49+
## Refer to [enum ModLoaderLog.VERBOSITY_LEVEL] for more details.
1650
@export var log_level := ModLoaderLog.VERBOSITY_LEVEL.DEBUG
1751
## Stops the mod loader from logging any deprecation related errors.
1852
@export var ignore_deprecated_errors: bool = false
@@ -43,6 +77,7 @@ extends Resource
4377
## Path to a folder containing mods [br]
4478
## Mod zips should be directly in this folder
4579
@export_dir var override_path_to_mods = ""
80+
## Use this option to override the default path where configs are stored.
4681
@export_dir var override_path_to_configs = ""
4782
## Path to a folder containing workshop items.[br]
4883
## Mods zips are placed in another folder, usually[br]
@@ -62,3 +97,17 @@ extends Resource
6297
@export_dir var restart_notification_scene_path := "res://addons/mod_loader/restart_notification.tscn"
6398
## Can be used to disable the mod loader's restart logic. Use the [signal ModLoader.new_hooks_created] to implement your own restart logic.
6499
@export var disable_restart := false
100+
101+
@export_group("Mod Validation")
102+
## Defines how the game version should be validated.
103+
## This setting controls validation for the game version specified in [member semantic_version]
104+
## and the mod's [member ModManifest.compatible_game_version].
105+
@export var game_version_validation := VERSION_VALIDATION.DEFAULT
106+
107+
## Callable that is executed during [ModManifest] validation
108+
## if [member game_version_validation] is set to [enum VERSION_VALIDATION] [code]CUSTOM[/code].
109+
## See the example under [enum VERSION_VALIDATION] [code]CUSTOM[/code] to learn how to set this up.
110+
var custom_game_version_validation_callable: Callable
111+
112+
## Stores the instance of the script specified in [member customize_script_path].
113+
var customize_script_instance: RefCounted

test/Unit/test_options.gd

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
extends GutTest
2+
3+
4+
func load_manifest_test_mod_1() -> ModManifest:
5+
var mod_path := "res://mods-unpacked/test-mod1/"
6+
var manifest_data: Dictionary = _ModLoaderFile.load_manifest_file(mod_path)
7+
8+
return ModManifest.new(manifest_data, mod_path)
9+
10+
11+
func test_customize_script() -> void:
12+
ModLoaderStore._update_ml_options_from_options_resource("res://test_options/customize_script/options_custom_validation.tres")
13+
var manifest := load_manifest_test_mod_1()
14+
15+
assert_eq(
16+
"".join(manifest.validation_messages_warning),
17+
"! ☞゚ヮ゚)☞ CUSTOM VALIDATION HERE ☜゚ヮ゚☜) !"
18+
)
19+
20+
21+
func test_customize_script_no_callable() -> void:
22+
# Clear saved error logs before testing to prevent false positives.
23+
ModLoaderLog.logged_messages.by_type.error.clear()
24+
25+
ModLoaderStore._update_ml_options_from_options_resource("res://test_options/customize_script_no_callable_set/options_custom_validation_no_callable_set.tres")
26+
var manifest := load_manifest_test_mod_1()
27+
28+
var logs := ModLoaderLog.get_by_type_as_string("error")
29+
30+
assert_string_contains("".join(logs), "No custom game version validation callable detected. Please provide a valid validation callable.")
31+
32+
33+
func test_game_verion_validation_disabled() -> void:
34+
ModLoaderStore._update_ml_options_from_options_resource("res://test_options/game_version_validation_disabled/options_game_version_validation_disabled.tres")
35+
var manifest := load_manifest_test_mod_1()
36+
37+
assert_true(manifest.validation_messages_error.size() == 0)
38+
39+
40+
func test_game_verion_validation_default() -> void:
41+
ModLoaderStore._update_ml_options_from_options_resource("res://test_options/game_version_validation_default/options_game_version_validation_default.tres")
42+
var manifest := load_manifest_test_mod_1()
43+
44+
assert_eq(
45+
"".join(manifest.validation_messages_error).replace("\r", "").replace("\n", "").replace("\t", ""),
46+
"The mod \"test-mod1\" is incompatible with the current game version.(current game version: 1000.0.0, mod compatible with game versions: [\"0.0.1\"])"
47+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[gd_resource type="Resource" script_class="ModLoaderOptionsProfile" load_steps=2 format=3 uid="uid://dky5648t3gmp2"]
2+
3+
[ext_resource type="Script" path="res://addons/mod_loader/resources/options_profile.gd" id="1_3rpjy"]
4+
5+
[resource]
6+
script = ExtResource("1_3rpjy")
7+
enable_mods = true
8+
locked_mods = Array[String]([])
9+
disabled_mods = Array[String]([])
10+
allow_modloader_autoloads_anywhere = false
11+
customize_script_path = "res://test_options/customize_script/customize_script.gd"
12+
log_level = 3
13+
ignore_deprecated_errors = false
14+
ignored_mod_names_in_log = Array[String]([])
15+
steam_id = 0
16+
semantic_version = "0.0.0"
17+
load_from_steam_workshop = false
18+
load_from_local = true
19+
override_path_to_mods = ""
20+
override_path_to_configs = ""
21+
override_path_to_workshop = ""
22+
override_path_to_hook_pack = ""
23+
override_hook_pack_name = ""
24+
restart_notification_scene_path = "res://addons/mod_loader/restart_notification.tscn"
25+
disable_restart = false
26+
game_version_validation = 2
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
extends RefCounted
2+
3+
# This is an example script for the ModLoaderOptionsProfile `customize_script_path`.
4+
# Ideally, place this script outside the `mod_loader` directory to simplify the update process.
5+
6+
7+
# This script is loaded after `mod_loader_store.ml_options` has been initialized.
8+
# It receives `ml_options` as an argument, allowing you to apply settings
9+
# that cannot be configured through the editor UI.
10+
func _init(ml_options: ModLoaderOptionsProfile) -> void:
11+
# Use OS.has_feature() to apply changes only for specific platforms,
12+
# or create multiple customization scripts and set their paths accordingly in the option profiles.
13+
if OS.has_feature("Steam"):
14+
pass
15+
elif OS.has_feature("Epic"):
16+
pass
17+
else:
18+
# Set `custom_game_version_validation_callable` to use a custom validation function.
19+
ml_options.custom_game_version_validation_callable = custom_is_game_version_compatible
20+
21+
22+
# Custom validation function
23+
# See `ModManifest._is_game_version_compatible()` for the default validation logic.
24+
func custom_is_game_version_compatible(manifest: ModManifest) -> bool:
25+
manifest.validation_messages_warning.push_back("! ☞゚ヮ゚)☞ CUSTOM VALIDATION HERE ☜゚ヮ゚☜) !")
26+
return true
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[gd_resource type="Resource" script_class="ModLoaderCurrentOptions" load_steps=3 format=3 uid="uid://d08kklljebrnh"]
2+
3+
[ext_resource type="Resource" uid="uid://dky5648t3gmp2" path="res://test_options/customize_script/custom_validation.tres" id="1_s4sec"]
4+
[ext_resource type="Script" path="res://addons/mod_loader/resources/options_current.gd" id="2_1rct1"]
5+
6+
[resource]
7+
script = ExtResource("2_1rct1")
8+
current_options = ExtResource("1_s4sec")
9+
feature_override_options = {}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[gd_resource type="Resource" script_class="ModLoaderOptionsProfile" load_steps=2 format=3 uid="uid://1gab2n8lgi60"]
2+
3+
[ext_resource type="Script" path="res://addons/mod_loader/resources/options_profile.gd" id="1_d2tfu"]
4+
5+
[resource]
6+
script = ExtResource("1_d2tfu")
7+
enable_mods = true
8+
locked_mods = Array[String]([])
9+
disabled_mods = Array[String]([])
10+
allow_modloader_autoloads_anywhere = false
11+
customize_script_path = "res://test_options/customize_script_no_callable_set/customize_script_no_callable_set.gd"
12+
log_level = 3
13+
ignore_deprecated_errors = false
14+
ignored_mod_names_in_log = Array[String]([])
15+
steam_id = 0
16+
semantic_version = "0.0.0"
17+
load_from_steam_workshop = false
18+
load_from_local = true
19+
override_path_to_mods = ""
20+
override_path_to_configs = ""
21+
override_path_to_workshop = ""
22+
override_path_to_hook_pack = ""
23+
override_hook_pack_name = ""
24+
restart_notification_scene_path = "res://addons/mod_loader/restart_notification.tscn"
25+
disable_restart = false
26+
game_version_validation = 2
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
extends RefCounted
2+
3+
# This is an example script for the ModLoaderOptionsProfile `customize_script_path`.
4+
# Ideally, place this script outside the `mod_loader` directory to simplify the update process.
5+
6+
7+
# This script is loaded after `mod_loader_store.ml_options` has been initialized.
8+
# It receives `ml_options` as an argument, allowing you to apply settings
9+
# that cannot be configured through the editor UI.
10+
func _init(ml_options: ModLoaderOptionsProfile) -> void:
11+
# Use OS.has_feature() to apply changes only for specific platforms,
12+
# or create multiple customization scripts and set their paths accordingly in the option profiles.
13+
if OS.has_feature("Steam"):
14+
pass
15+
elif OS.has_feature("Epic"):
16+
pass
17+
else:
18+
pass
19+
# Set `custom_game_version_validation_callable` to use a custom validation function.
20+
#ml_options.custom_game_version_validation_callable = custom_is_game_version_compatible
21+
22+
23+
# Custom validation function
24+
# See `ModManifest._is_game_version_compatible()` for the default validation logic.
25+
func custom_is_game_version_compatible(manifest: ModManifest) -> bool:
26+
manifest.validation_messages_warning.push_back("! ☞゚ヮ゚)☞ CUSTOM VALIDATION HERE ☜゚ヮ゚☜) !")
27+
return true
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[gd_resource type="Resource" script_class="ModLoaderCurrentOptions" load_steps=3 format=3 uid="uid://c25j7kt7y8ora"]
2+
3+
[ext_resource type="Resource" uid="uid://1gab2n8lgi60" path="res://test_options/customize_script_no_callable_set/custom_validation_no_callable_set.tres" id="1_xrqi6"]
4+
[ext_resource type="Script" path="res://addons/mod_loader/resources/options_current.gd" id="2_4o6bw"]
5+
6+
[resource]
7+
script = ExtResource("2_4o6bw")
8+
current_options = ExtResource("1_xrqi6")
9+
feature_override_options = {}

0 commit comments

Comments
 (0)