Skip to content

Commit

Permalink
feat: ✨ options for game version validation (#536)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
KANAjetzt and Qubus0 authored Feb 6, 2025
1 parent a0b3872 commit 6119166
Show file tree
Hide file tree
Showing 14 changed files with 306 additions and 8 deletions.
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 @@ -129,11 +129,10 @@ func _exit_tree() -> void:


# 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 @@ -183,6 +182,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)


# Update ModLoader's options, via CLI args
func _update_ml_options_from_cli_args() -> void:
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://wiki.godotmodding.com/guides/integration/mod_loader_options/#game-version-validation


## 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 @@ -43,6 +77,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 @@ -62,3 +97,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 = {}

0 comments on commit 6119166

Please sign in to comment.