Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
692652c
feat: :sparkles: option to disable game version validation
KANAjetzt Jan 30, 2025
4a743cb
feat: :sparkles: added custom validation option
KANAjetzt Jan 31, 2025
8a19d72
docs: :memo: added missing doc comments
KANAjetzt Jan 31, 2025
2dc7e5a
refactor: :recycle: use ENUM
KANAjetzt Feb 2, 2025
d342385
refactor: :recycle: only pass `ml_options`
KANAjetzt Feb 2, 2025
24f723c
refactor: :recycle: move `customize_script_path` out of export group
KANAjetzt Feb 2, 2025
172e0ff
docs: :memo: added example customize script
KANAjetzt Feb 2, 2025
581684f
style: :pencil2: improved spelling
KANAjetzt Feb 2, 2025
67127e3
docs: :memo: reworked comments
KANAjetzt Feb 2, 2025
666f4c6
refactor: :recycle: `ml_options_path` as param
KANAjetzt Feb 5, 2025
00b9292
refactor: :fire: remove example script
KANAjetzt Feb 5, 2025
013b790
refactor: :recycle: removed example added `@tutorial`
KANAjetzt Feb 5, 2025
b97fab7
test: :test_tube: added custom validation test
KANAjetzt Feb 5, 2025
c354ae0
fix: :test_tube: fixed test setup
KANAjetzt Feb 5, 2025
0743144
fix: :test_tube: removed editor override
KANAjetzt Feb 5, 2025
9ef663b
fix: :bug: set `customize_script_path` outside of for loop
KANAjetzt Feb 5, 2025
df193ca
refactor: :truck: added sub dir
KANAjetzt Feb 5, 2025
8b122c3
test: :test_tube: added test for game version validation disabled
KANAjetzt Feb 5, 2025
52c582a
fix: :test_tube: updated custom script path
KANAjetzt Feb 5, 2025
8d12b7a
test: :test_tube: added `test_game_verion_validation_default`
KANAjetzt Feb 5, 2025
b8ef08a
fix: :test_tube: replace white space chars with `""`
KANAjetzt Feb 5, 2025
c26a4bf
refactor: :recycle: clean up a bit
KANAjetzt Feb 5, 2025
d136db4
test: :test_tube: added no callable set test
KANAjetzt Feb 5, 2025
b34413e
Update addons/mod_loader/resources/options_profile.gd
KANAjetzt Feb 5, 2025
7eea1d7
Update addons/mod_loader/resources/options_profile.gd
KANAjetzt Feb 6, 2025
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
12 changes: 7 additions & 5 deletions addons/mod_loader/mod_loader_store.gd
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,10 @@ func _init():


# Update ModLoader's options, via the custom options resource
func _update_ml_options_from_options_resource() -> void:
# Path to the options resource
# See: res://addons/mod_loader/resources/options_current.gd
var ml_options_path := "res://addons/mod_loader/options/options.tres"

#
# Parameters:
# - ml_options_path: Path to the options resource. See: res://addons/mod_loader/resources/options_current.gd
func _update_ml_options_from_options_resource(ml_options_path := "res://addons/mod_loader/options/options.tres") -> void:
# Get user options for ModLoader
if not _ModLoaderFile.file_exists(ml_options_path) and not ResourceLoader.exists(ml_options_path):
ModLoaderLog.fatal(str("A critical file is missing: ", ml_options_path), LOG_NAME)
Expand Down Expand Up @@ -177,6 +176,9 @@ func _update_ml_options_from_options_resource() -> void:
# Update from the options in the resource
ml_options = override_options

if not ml_options.customize_script_path.is_empty():
ml_options.customize_script_instance = load(ml_options.customize_script_path).new(ml_options)


func _exit_tree() -> void:
# Save the cache to the cache file.
Expand Down
9 changes: 8 additions & 1 deletion addons/mod_loader/resources/mod_manifest.gd
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,14 @@ func validate(manifest: Dictionary, path: String) -> bool:
config_schema = ModLoaderUtils.get_dict_from_dict(godot_details, "config_schema")
steam_workshop_id = ModLoaderUtils.get_string_from_dict(godot_details, "steam_workshop_id")

_is_game_version_compatible(mod_id)
if ModLoaderStore.ml_options.game_version_validation == ModLoaderOptionsProfile.VERSION_VALIDATION.DEFAULT:
_is_game_version_compatible(mod_id)

if ModLoaderStore.ml_options.game_version_validation == ModLoaderOptionsProfile.VERSION_VALIDATION.CUSTOM:
if ModLoaderStore.ml_options.custom_game_version_validation_callable:
ModLoaderStore.ml_options.custom_game_version_validation_callable.call(self)
else:
ModLoaderLog.error("No custom game version validation callable detected. Please provide a valid validation callable.", LOG_NAME)

is_mod_id_array_valid(mod_id, dependencies, "dependency")
is_mod_id_array_valid(mod_id, incompatibilities, "incompatibility")
Expand Down
53 changes: 51 additions & 2 deletions addons/mod_loader/resources/options_profile.gd
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
class_name ModLoaderOptionsProfile
extends Resource
##
## Class to define and store Mod Loader Options.
##
## @tutorial(Example Customization Script): https://example.com


## Settings for game version validation.
enum VERSION_VALIDATION {
## Uses the default semantic versioning (semver) validation.
DEFAULT,

## Disables validation of the game version specified in [member semantic_version]
## and the mod's [member ModManifest.compatible_game_version].
DISABLED,

## Enables custom game version validation.
## Use [member customize_script_path] to specify a script that customizes the Mod Loader options.
## In this script, you must set [member custom_game_version_validation_callable]
## to a custom validation [Callable].
## [br]
## ===[br]
## [b]Note:[color=note "Easier Mod Loader Updates"][/color][/b][br]
## Using a custom script allows you to keep your code outside the addons directory,
## making it easier to update the mod loader without affecting your modifications. [br]
## ===[br]
CUSTOM,
}

## Can be used to disable mods for specific plaforms by using feature overrides
@export var enable_mods: bool = true
## List of mod ids that can't be turned on or off
@export var locked_mods: Array[String] = []

## List of mods that will not be loaded
@export var disabled_mods: Array[String] = []
## Disables the requirement for the mod loader autoloads to be first
@export var allow_modloader_autoloads_anywhere: bool = false

## This script is loaded after [member ModLoaderStore.ml_options] has been initialized.
## It is instantiated with [member ModLoaderStore.ml_options] as an argument.
## Use this script to apply settings that cannot be configured through the editor UI.
##
## For an example, see [enum VERSION_VALIDATION] [code]CUSTOM[/code] or
## [code]res://addons/mod_loader/options/example_customize_script.gd[/code].
@export_file var customize_script_path: String

@export_group("Logging")
## Sets the logging verbosity level.
## Refer to [enum ModLoaderLog.VERBOSITY_LEVEL] for more details.
@export var log_level := ModLoaderLog.VERBOSITY_LEVEL.DEBUG
## Stops the mod loader from logging any deprecation related errors.
@export var ignore_deprecated_errors: bool = false
Expand Down Expand Up @@ -42,6 +76,7 @@ extends Resource
## Path to a folder containing mods [br]
## Mod zips should be directly in this folder
@export_dir var override_path_to_mods = ""
## Use this option to override the default path where configs are stored.
@export_dir var override_path_to_configs = ""
## Path to a folder containing workshop items.[br]
## Mods zips are placed in another folder, usually[br]
Expand All @@ -61,3 +96,17 @@ extends Resource
@export_dir var restart_notification_scene_path := "res://addons/mod_loader/restart_notification.tscn"
## Can be used to disable the mod loader's restart logic. Use the [signal ModLoader.new_hooks_created] to implement your own restart logic.
@export var disable_restart := false

@export_group("Mod Validation")
## Defines how the game version should be validated.
## This setting controls validation for the game version specified in [member semantic_version]
## and the mod's [member ModManifest.compatible_game_version].
@export var game_version_validation := VERSION_VALIDATION.DEFAULT

## Callable that is executed during [ModManifest] validation
## if [member game_version_validation] is set to [enum VERSION_VALIDATION] [code]CUSTOM[/code].
## See the example under [enum VERSION_VALIDATION] [code]CUSTOM[/code] to learn how to set this up.
var custom_game_version_validation_callable: Callable

## Stores the instance of the script specified in [member customize_script_path].
var customize_script_instance: RefCounted
47 changes: 47 additions & 0 deletions test/Unit/test_options.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
extends GutTest


func load_manifest_test_mod_1() -> ModManifest:
var mod_path := "res://mods-unpacked/test-mod1/"
var manifest_data: Dictionary = _ModLoaderFile.load_manifest_file(mod_path)

return ModManifest.new(manifest_data, mod_path)


func test_customize_script() -> void:
ModLoaderStore._update_ml_options_from_options_resource("res://test_options/customize_script/options_custom_validation.tres")
var manifest := load_manifest_test_mod_1()

assert_eq(
"".join(manifest.validation_messages_warning),
"! ☞゚ヮ゚)☞ CUSTOM VALIDATION HERE ☜゚ヮ゚☜) !"
)


func test_customize_script_no_callable() -> void:
# Clear saved error logs before testing to prevent false positives.
ModLoaderLog.logged_messages.by_type.error.clear()

ModLoaderStore._update_ml_options_from_options_resource("res://test_options/customize_script_no_callable_set/options_custom_validation_no_callable_set.tres")
var manifest := load_manifest_test_mod_1()

var logs := ModLoaderLog.get_by_type_as_string("error")

assert_string_contains("".join(logs), "No custom game version validation callable detected. Please provide a valid validation callable.")


func test_game_verion_validation_disabled() -> void:
ModLoaderStore._update_ml_options_from_options_resource("res://test_options/game_version_validation_disabled/options_game_version_validation_disabled.tres")
var manifest := load_manifest_test_mod_1()

assert_true(manifest.validation_messages_error.size() == 0)


func test_game_verion_validation_default() -> void:
ModLoaderStore._update_ml_options_from_options_resource("res://test_options/game_version_validation_default/options_game_version_validation_default.tres")
var manifest := load_manifest_test_mod_1()

assert_eq(
"".join(manifest.validation_messages_error).replace("\r", "").replace("\n", "").replace("\t", ""),
"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\"])"
)
26 changes: 26 additions & 0 deletions test/test_options/customize_script/custom_validation.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[gd_resource type="Resource" script_class="ModLoaderOptionsProfile" load_steps=2 format=3 uid="uid://dky5648t3gmp2"]

[ext_resource type="Script" path="res://addons/mod_loader/resources/options_profile.gd" id="1_3rpjy"]

[resource]
script = ExtResource("1_3rpjy")
enable_mods = true
locked_mods = Array[String]([])
disabled_mods = Array[String]([])
allow_modloader_autoloads_anywhere = false
customize_script_path = "res://test_options/customize_script/customize_script.gd"
log_level = 3
ignore_deprecated_errors = false
ignored_mod_names_in_log = Array[String]([])
steam_id = 0
semantic_version = "0.0.0"
load_from_steam_workshop = false
load_from_local = true
override_path_to_mods = ""
override_path_to_configs = ""
override_path_to_workshop = ""
override_path_to_hook_pack = ""
override_hook_pack_name = ""
restart_notification_scene_path = "res://addons/mod_loader/restart_notification.tscn"
disable_restart = false
game_version_validation = 2
26 changes: 26 additions & 0 deletions test/test_options/customize_script/customize_script.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
extends RefCounted

# This is an example script for the ModLoaderOptionsProfile `customize_script_path`.
# Ideally, place this script outside the `mod_loader` directory to simplify the update process.


# This script is loaded after `mod_loader_store.ml_options` has been initialized.
# It receives `ml_options` as an argument, allowing you to apply settings
# that cannot be configured through the editor UI.
func _init(ml_options: ModLoaderOptionsProfile) -> void:
# Use OS.has_feature() to apply changes only for specific platforms,
# or create multiple customization scripts and set their paths accordingly in the option profiles.
if OS.has_feature("Steam"):
pass
elif OS.has_feature("Epic"):
pass
else:
# Set `custom_game_version_validation_callable` to use a custom validation function.
ml_options.custom_game_version_validation_callable = custom_is_game_version_compatible


# Custom validation function
# See `ModManifest._is_game_version_compatible()` for the default validation logic.
func custom_is_game_version_compatible(manifest: ModManifest) -> bool:
manifest.validation_messages_warning.push_back("! ☞゚ヮ゚)☞ CUSTOM VALIDATION HERE ☜゚ヮ゚☜) !")
return true
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ModLoaderCurrentOptions" load_steps=3 format=3 uid="uid://d08kklljebrnh"]

[ext_resource type="Resource" uid="uid://dky5648t3gmp2" path="res://test_options/customize_script/custom_validation.tres" id="1_s4sec"]
[ext_resource type="Script" path="res://addons/mod_loader/resources/options_current.gd" id="2_1rct1"]

[resource]
script = ExtResource("2_1rct1")
current_options = ExtResource("1_s4sec")
feature_override_options = {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[gd_resource type="Resource" script_class="ModLoaderOptionsProfile" load_steps=2 format=3 uid="uid://1gab2n8lgi60"]

[ext_resource type="Script" path="res://addons/mod_loader/resources/options_profile.gd" id="1_d2tfu"]

[resource]
script = ExtResource("1_d2tfu")
enable_mods = true
locked_mods = Array[String]([])
disabled_mods = Array[String]([])
allow_modloader_autoloads_anywhere = false
customize_script_path = "res://test_options/customize_script_no_callable_set/customize_script_no_callable_set.gd"
log_level = 3
ignore_deprecated_errors = false
ignored_mod_names_in_log = Array[String]([])
steam_id = 0
semantic_version = "0.0.0"
load_from_steam_workshop = false
load_from_local = true
override_path_to_mods = ""
override_path_to_configs = ""
override_path_to_workshop = ""
override_path_to_hook_pack = ""
override_hook_pack_name = ""
restart_notification_scene_path = "res://addons/mod_loader/restart_notification.tscn"
disable_restart = false
game_version_validation = 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
extends RefCounted

# This is an example script for the ModLoaderOptionsProfile `customize_script_path`.
# Ideally, place this script outside the `mod_loader` directory to simplify the update process.


# This script is loaded after `mod_loader_store.ml_options` has been initialized.
# It receives `ml_options` as an argument, allowing you to apply settings
# that cannot be configured through the editor UI.
func _init(ml_options: ModLoaderOptionsProfile) -> void:
# Use OS.has_feature() to apply changes only for specific platforms,
# or create multiple customization scripts and set their paths accordingly in the option profiles.
if OS.has_feature("Steam"):
pass
elif OS.has_feature("Epic"):
pass
else:
pass
# Set `custom_game_version_validation_callable` to use a custom validation function.
#ml_options.custom_game_version_validation_callable = custom_is_game_version_compatible


# Custom validation function
# See `ModManifest._is_game_version_compatible()` for the default validation logic.
func custom_is_game_version_compatible(manifest: ModManifest) -> bool:
manifest.validation_messages_warning.push_back("! ☞゚ヮ゚)☞ CUSTOM VALIDATION HERE ☜゚ヮ゚☜) !")
return true
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ModLoaderCurrentOptions" load_steps=3 format=3 uid="uid://c25j7kt7y8ora"]

[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"]
[ext_resource type="Script" path="res://addons/mod_loader/resources/options_current.gd" id="2_4o6bw"]

[resource]
script = ExtResource("2_4o6bw")
current_options = ExtResource("1_xrqi6")
feature_override_options = {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[gd_resource type="Resource" script_class="ModLoaderOptionsProfile" load_steps=2 format=3 uid="uid://bnc6gslxpnx3y"]

[ext_resource type="Script" path="res://addons/mod_loader/resources/options_profile.gd" id="1_kdajl"]

[resource]
script = ExtResource("1_kdajl")
enable_mods = true
locked_mods = Array[String]([])
disabled_mods = Array[String]([])
allow_modloader_autoloads_anywhere = false
customize_script_path = ""
log_level = 3
ignore_deprecated_errors = false
ignored_mod_names_in_log = Array[String]([])
steam_id = 0
semantic_version = "1000.0.0"
load_from_steam_workshop = false
load_from_local = true
override_path_to_mods = ""
override_path_to_configs = ""
override_path_to_workshop = ""
override_path_to_hook_pack = ""
override_hook_pack_name = ""
restart_notification_scene_path = "res://addons/mod_loader/restart_notification.tscn"
disable_restart = false
game_version_validation = 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ModLoaderCurrentOptions" load_steps=3 format=3 uid="uid://emmn66l0e1n0"]

[ext_resource type="Resource" uid="uid://bnc6gslxpnx3y" path="res://test_options/game_version_validation_default/game_version_validation_default.tres" id="1_ey6sk"]
[ext_resource type="Script" path="res://addons/mod_loader/resources/options_current.gd" id="2_0ultl"]

[resource]
script = ExtResource("2_0ultl")
current_options = ExtResource("1_ey6sk")
feature_override_options = {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[gd_resource type="Resource" script_class="ModLoaderOptionsProfile" load_steps=2 format=3 uid="uid://d2ktmje1gd5vb"]

[ext_resource type="Script" path="res://addons/mod_loader/resources/options_profile.gd" id="1_vd02r"]

[resource]
script = ExtResource("1_vd02r")
enable_mods = true
locked_mods = Array[String]([])
disabled_mods = Array[String]([])
allow_modloader_autoloads_anywhere = false
customize_script_path = ""
log_level = 3
ignore_deprecated_errors = false
ignored_mod_names_in_log = Array[String]([])
steam_id = 0
semantic_version = "1000.0.0"
load_from_steam_workshop = false
load_from_local = true
override_path_to_mods = ""
override_path_to_configs = ""
override_path_to_workshop = ""
override_path_to_hook_pack = ""
override_hook_pack_name = ""
restart_notification_scene_path = "res://addons/mod_loader/restart_notification.tscn"
disable_restart = false
game_version_validation = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ModLoaderCurrentOptions" load_steps=3 format=3 uid="uid://dsegljus5l2qm"]

[ext_resource type="Resource" uid="uid://d2ktmje1gd5vb" path="res://test_options/game_version_validation_disabled/game_version_validation_disabled.tres" id="1_18vx8"]
[ext_resource type="Script" path="res://addons/mod_loader/resources/options_current.gd" id="2_5hgrx"]

[resource]
script = ExtResource("2_5hgrx")
current_options = ExtResource("1_18vx8")
feature_override_options = {}