diff --git a/.github/actions/godot-install/action.yml b/.github/actions/godot-install/action.yml new file mode 100644 index 00000000..4f0fd92d --- /dev/null +++ b/.github/actions/godot-install/action.yml @@ -0,0 +1,69 @@ +# Inspired by https://github.com/bitbrain/beehave/blob/godot-4.x/.github/actions/godot-install/action.yml + +name: install-godot-binary +description: "Installs the Godot Runtime" + +inputs: + godot-version: + description: "The Godot engine version" + type: string + required: true + godot-status-version: + description: "The Godot engine status version" + type: string + required: true + godot-bin-name: + type: string + required: true + godot-cache-path: + type: string + required: true + + +runs: + using: composite + steps: + + - name: "Set Cache Name" + shell: bash + run: | + echo "CACHE_NAME=${{ runner.OS }}-Godot_v${{ inputs.godot-version }}-${{ inputs.godot-status-version }}" >> "$GITHUB_ENV" + + - name: "Godot Cache Restore" + uses: actions/cache/restore@v3 + id: godot-restore-cache + with: + path: ${{ inputs.godot-cache-path }} + key: ${{ env.CACHE_NAME }} + + - name: "Download and Install Godot ${{ inputs.godot-version }}" + if: steps.godot-restore-cache.outputs.cache-hit != 'true' + continue-on-error: false + shell: bash + run: | + mkdir -p ${{ inputs.godot-cache-path }} + chmod 770 ${{ inputs.godot-cache-path }} + DIR="$HOME/.config/godot" + if [ ! -d "$DIR" ]; then + mkdir -p "$DIR" + chmod 770 "$DIR" + fi + + DOWNLOAD_URL=https://github.com/godotengine/godot/releases/download/${{ inputs.godot-version }}-${{ inputs.godot-status-version }} + GODOT_BIN=Godot_v${{ inputs.godot-version }}-${{ inputs.godot-status-version }}_${{ inputs.godot-bin-name }} + + GODOT_PACKAGE=$GODOT_BIN.zip + wget $DOWNLOAD_URL/$GODOT_PACKAGE -P ${{ inputs.godot-cache-path }} + unzip ${{ inputs.godot-cache-path }}/$GODOT_PACKAGE -d ${{ inputs.godot-cache-path }} + + mv ${{ inputs.godot-cache-path }}/$GODOT_BIN ${{ inputs.godot-cache-path }}/godot + + chmod u+x ${{ inputs.godot-cache-path }}/godot + echo "${{ inputs.godot-cache-path }}/godot" + + - name: "Godot Cache Save" + if: steps.godot-restore-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v3 + with: + path: ${{ inputs.godot-cache-path }} + key: ${{ steps.godot-restore-cache.outputs.cache-primary-key }} \ No newline at end of file diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml new file mode 100644 index 00000000..0b15285e --- /dev/null +++ b/.github/actions/test/action.yml @@ -0,0 +1,80 @@ +# Inspired by https://github.com/utopia-rise/fmod-gdextension/blob/godot-3.x/demo/run_tests.sh + +name: test +description: "Runs the tests via GUT CLI" + +inputs: + gut-download-path: + required: true + default: ~/gut_download + gut-addons-path: + required: true + default: ${{ github.workspace }}/test/addons/gut + godot-test-project: + required: true + default: ${{ github.workspace }}/test + +runs: + using: composite + + steps: + + - name: "Set Cache Name" + shell: bash + run: | + echo "CACHE_NAME_GUT=GUT_v7.4.1" >> "$GITHUB_ENV" + + - name: "GUT Cache Restore" + uses: actions/cache/restore@v3 + id: gut-restore-cache + with: + path: ${{ inputs.gut-download-path }} + key: ${{ runner.os }}-${{ env.CACHE_NAME_GUT }} + + - name: "Download GUT" + if: steps.gut-restore-cache.outputs.cache-hit != 'true' + continue-on-error: false + shell: bash + run: | + mkdir -p ${{ inputs.gut-download-path }} + chmod 770 ${{ inputs.gut-download-path }} + + wget https://github.com/bitwes/Gut/archive/refs/tags/v7.4.1.zip -P ${{ inputs.gut-download-path }} + unzip ${{ inputs.gut-download-path }}/v7.4.1.zip -d ${{ inputs.gut-download-path }}/unzip + + - name: "GUT Cache Save" + if: steps.gut-restore-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v3 + with: + path: ${{ inputs.gut-download-path }} + key: ${{ steps.gut-restore-cache.outputs.cache-primary-key }} + + - name: "Create addons Directory" + if: ${{ !cancelled() }} + shell: bash + run: mkdir -p ${{ github.workspace }}/test/addons + + - name: "โš” Link GUT" + if: ${{ !cancelled() }} + shell: bash + run: ln -s ${{ inputs.gut-download-path }}/unzip/Gut-7.4.1/addons/gut ${{ github.workspace }}/test/addons/gut + + - name: "โš” Link Mod Loader" + if: ${{ !cancelled() }} + shell: bash + run: ln -s ${{ github.workspace }}/addons/mod_loader ${{ github.workspace }}/test/addons/mod_loader + + - name: "โš” Link JSON_Schema_Validator" + if: ${{ !cancelled() }} + shell: bash + run: ln -s ${{ github.workspace }}/addons/JSON_Schema_Validator ${{ github.workspace }}/test/addons/JSON_Schema_Validator + + - name: "Run Tests" + if: ${{ runner.OS == 'Linux'}} && ${{ !cancelled() }} + env: + TEST_PROJECT: ${{ inputs.godot-test-project }} + shell: bash + run: | + cd "${TEST_PROJECT}" + chmod +x run_tests.sh + ./run_tests.sh "$HOME/godot-linux/godot" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00d1a2d1..8f02bf84 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,30 @@ -on: [pull_request] +# Inspired by https://github.com/bitbrain/beehave/blob/godot-4.x/.github/workflows/beehave-ci.yml + +name: Mod Loader CI + +on: + push: + paths-ignore: + - '**.jpg' + - '**.png' + - '**.svg' + - '**.md' + - '**plugin.cfg' + pull_request: + paths-ignore: + - '**.jpg' + - '**.png' + - '**.svg' + - '**.md' + - '**plugin.cfg' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - check_dependencies: - runs-on: ubuntu-latest - name: PR Dependency Check - steps: - - uses: gregsdennis/dependencies-action@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + tests: + name: "Running GUT tests on Godot 3.5.3" + uses: ./.github/workflows/tests.yml + with: + godot-version: '3.5.3' \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..3ff7a30a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +# Inspired by https://github.com/bitbrain/beehave/blob/godot-4.x/.github/workflows/unit-tests.yml + +name: tests +run-name: ${{ github.head_ref || github.ref_name }}-tests + +on: + workflow_call: + inputs: + os: + required: false + type: string + default: 'ubuntu-22.04' + godot-version: + required: true + type: string + default: '3.5.3' + + workflow_dispatch: + inputs: + os: + required: false + type: string + default: 'ubuntu-22.04' + godot-version: + required: true + type: string + +concurrency: + group: tests-${{ github.head_ref || github.ref_name }}-${{ inputs.godot-version }} + cancel-in-progress: true + +jobs: + test: + name: "Tests" + runs-on: ${{ inputs.os }} + timeout-minutes: 15 + + steps: + - name: "๐Ÿ“ฆ Checkout Mod Loader Repository" + uses: actions/checkout@v4 + with: + lfs: true + submodules: 'recursive' + + - name: "๐Ÿค– Install Godot ${{ inputs.godot-version }}" + uses: ./.github/actions/godot-install + with: + godot-version: ${{ inputs.godot-version }} + godot-status-version: 'stable' + godot-bin-name: 'linux_headless.64' + godot-cache-path: '~/godot-linux' + + - name: "๐Ÿงช Run Tests" + if: ${{ !cancelled() }} + timeout-minutes: 4 + uses: ./.github/actions/test + with: + godot-test-project: ${{ github.workspace }}/test \ No newline at end of file diff --git a/README.md b/README.md index 9737e682..fcd7740e 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,14 @@ You can find detailed documentation, for game and mod developers, on the [Wiki]( 4. Use the [API Methods](https://wiki.godotmodding.com/#/api/mod_loader_api) *A list of all available API Methods.* -## Godot Version -The current version of the Mod Loader is developed for Godot 3. The Godot 4 version is in progress on the [4.x branch](https://github.com/GodotModding/godot-mod-loader/tree/4.x) and can be used as long as no `class_name`s are in the project. Projects with `class_name`s are currently affected by an [engine bug](https://github.com/godotengine/godot/issues/83542). We are hopeful that this issue will be resolved in the near future. For more details and updates on the Godot 4 version, please follow this [issue](https://github.com/GodotModding/godot-mod-loader/issues/315) or join us on [our Discord](https://discord.godotmodding.com). +## Godot Version +The Mod Loader currently supports Godot 3.5 and later, as well as Godot 4.1 and later. Feel free to [open an issue](https://github.com/GodotModding/godot-mod-loader/issues/new) if you need support for a different version. -## Development -The latest work-in-progress build can be found on the [development branch](https://github.com/GodotModding/godot-mod-loader/tree/development). +## Development +The latest work-in-progress build for Godot 3 is available on the [development branch](https://github.com/GodotModding/godot-mod-loader/tree/development). For Godot 4, visit the [4.x branch](https://github.com/GodotModding/godot-mod-loader/tree/4.x). + +## Releases +You can find the latest release versions and detailed installation instructions on the [Releases Page](https://github.com/GodotModding/godot-mod-loader/releases). ## Compatibility The Mod Loader supports the following platforms: @@ -39,6 +42,9 @@ The Mod Loader supports the following platforms: - Android - iOS +## Keep in touche +For more details and updates join us on [our Discord](https://discord.godotmodding.com). + ## Games Made Moddable by This Project - [Brotato](https://store.steampowered.com/app/1942280/Brotato/) by [Blobfish Games](https://store.steampowered.com/developer/blobfishgames) diff --git a/addons/mod_loader/api/log.gd b/addons/mod_loader/api/log.gd index cc4b0772..61880f8d 100644 --- a/addons/mod_loader/api/log.gd +++ b/addons/mod_loader/api/log.gd @@ -385,15 +385,27 @@ static func _log(message: String, mod_name: String, log_type: String = "info", o _write_to_log_file(log_entry.get_entry()) -static func _is_mod_name_ignored(mod_name: String) -> bool: +static func _is_mod_name_ignored(mod_log_name: String) -> bool: if not ModLoaderStore: return false - var ignored_mod_names := ModLoaderStore.ml_options.ignored_mod_names_in_log as Array + var ignored_mod_log_names := ModLoaderStore.ml_options.ignored_mod_names_in_log as Array - if not ignored_mod_names.size() == 0: - if mod_name in ignored_mod_names: - return true + # No ignored mod names + if ignored_mod_log_names.size() == 0: + return false + + # Directly match a full mod log name. ex: "ModLoader:Deprecated" + if mod_log_name in ignored_mod_log_names: + return true + + # Match a mod log name with a wildcard. ex: "ModLoader:*" + for ignored_mod_name in ignored_mod_log_names: + if ignored_mod_name.ends_with("*"): + if mod_log_name.begins_with(ignored_mod_name.trim_suffix("*")): + return true + + # No match return false diff --git a/addons/mod_loader/api/mod.gd b/addons/mod_loader/api/mod.gd index 87ea3668..47c31a53 100644 --- a/addons/mod_loader/api/mod.gd +++ b/addons/mod_loader/api/mod.gd @@ -24,7 +24,7 @@ const LOG_NAME := "ModLoader:Mod" # Returns: void static func install_script_extension(child_script_path: String) -> void: - var mod_id: String = ModLoaderUtils.get_string_in_between(child_script_path, "res://mods-unpacked/", "/") + var mod_id: String = _ModLoaderPath.get_mod_dir(child_script_path) var mod_data: ModData = get_mod_data(mod_id) if not ModLoaderStore.saved_extension_paths.has(mod_data.manifest.get_mod_id()): ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()] = [] @@ -71,8 +71,12 @@ static func add_translation(resource_path: String) -> void: return var translation_object: Translation = load(resource_path) - TranslationServer.add_translation(translation_object) - ModLoaderLog.info("Added Translation from Resource -> %s" % resource_path, LOG_NAME) + if translation_object: + TranslationServer.add_translation(translation_object) + ModLoaderLog.info("Added Translation from Resource -> %s" % resource_path, LOG_NAME) + else: + ModLoaderLog.fatal("Failed to load translation at path: %s" % [resource_path], LOG_NAME) + # Appends a new node to a modified scene. diff --git a/addons/mod_loader/api/profile.gd b/addons/mod_loader/api/profile.gd index abac4287..eceee47c 100644 --- a/addons/mod_loader/api/profile.gd +++ b/addons/mod_loader/api/profile.gd @@ -95,6 +95,45 @@ static func create_profile(profile_name: String) -> bool: return is_save_success +# Renames an existing user profile. +# +# Parameters: +# - old_profile_name (String): The current name for the user profile (must be unique). +# - new_profile_name (String): The new name for the user profile (must be unique). +# +# Returns: bool +static func rename_profile(old_profile_name: String, new_profile_name: String) -> bool: + # Verify that the old profile name is already in use + if not ModLoaderStore.user_profiles.has(old_profile_name): + ModLoaderLog.error("User profile with the name of \"%s\" does not exist." % old_profile_name, LOG_NAME) + return false + + # Verify that the new profile_name is not already in use + if ModLoaderStore.user_profiles.has(new_profile_name): + ModLoaderLog.error("User profile with the name of \"%s\" already exists." % new_profile_name, LOG_NAME) + return false + + # Rename user profile + var profile_renamed := ModLoaderStore.user_profiles[old_profile_name].duplicate() as ModUserProfile + profile_renamed.name = new_profile_name + + # Remove old profile entry, replace it with new name entry in the ModLoaderStore + ModLoaderStore.user_profiles.erase(old_profile_name) + ModLoaderStore.user_profiles[new_profile_name] = profile_renamed + + # Set it as the current profile if it was the current profile + if ModLoaderStore.current_user_profile.name == old_profile_name: + set_profile(profile_renamed) + + # Store the new profile in the json file + var is_save_success := _save() + + if is_save_success: + ModLoaderLog.debug("Renamed user profile from \"%s\" to \"%s\"" % [old_profile_name, new_profile_name], LOG_NAME) + + return is_save_success + + # Sets the current user profile to the given user profile. # # Parameters: @@ -187,6 +226,13 @@ static func get_all_as_array() -> Array: return user_profiles +# Returns true if the Mod User Profiles are initialized. +# On the first execution of the game, user profiles might not yet be created. +# Use this method to check if everything is ready to interact with the ModLoaderUserProfile API. +static func is_initialized() -> bool: + return _ModLoaderFile.file_exists(FILE_PATH_USER_PROFILES) + + # Internal profile functions # ============================================================================= diff --git a/addons/mod_loader/internal/cache.gd b/addons/mod_loader/internal/cache.gd index 80972834..8277789c 100644 --- a/addons/mod_loader/internal/cache.gd +++ b/addons/mod_loader/internal/cache.gd @@ -31,7 +31,7 @@ static func add_data(key: String, data: Dictionary) -> Dictionary: # Get data from a specific key static func get_data(key: String) -> Dictionary: if not ModLoaderStore.cache.has(key): - ModLoaderLog.error("key: \"%s\" not found in \"ModLoaderStore.cache\"" % key, LOG_NAME) + ModLoaderLog.info("key: \"%s\" not found in \"ModLoaderStore.cache\"" % key, LOG_NAME) return {} return ModLoaderStore.cache[key] diff --git a/addons/mod_loader/internal/file.gd b/addons/mod_loader/internal/file.gd index 0c395a4f..01cb1a49 100644 --- a/addons/mod_loader/internal/file.gd +++ b/addons/mod_loader/internal/file.gd @@ -207,7 +207,13 @@ static func remove_file(file_path: String) -> bool: static func file_exists(path: String) -> bool: var file := File.new() - return file.file_exists(path) + var exists := file.file_exists(path) + + # If the file is not found, check if it has been remapped because it is a Resource. + if not exists: + exists = ResourceLoader.exists(path) + + return exists static func dir_exists(path: String) -> bool: diff --git a/addons/mod_loader/internal/mod_loader_utils.gd b/addons/mod_loader/internal/mod_loader_utils.gd index 31d905b7..1f99e24e 100644 --- a/addons/mod_loader/internal/mod_loader_utils.gd +++ b/addons/mod_loader/internal/mod_loader_utils.gd @@ -100,25 +100,6 @@ static func _is_valid_global_class_dict(global_class_dict: Dictionary) -> bool: return true -# Returns the string in between two strings in a provided string -static func get_string_in_between(string: String, initial: String, ending: String) -> String: - var start_index: int = string.find(initial) - if start_index == -1: - ModLoaderLog.error("Initial string not found.", LOG_NAME) - return "" - - start_index += initial.length() - - var end_index: int = string.find(ending, start_index) - if end_index == -1: - ModLoaderLog.error("Ending string not found.", LOG_NAME) - return "" - - var found_string: String = string.substr(start_index, end_index - start_index) - - return found_string - - # Deprecated # ============================================================================= diff --git a/addons/mod_loader/internal/path.gd b/addons/mod_loader/internal/path.gd index d4c9d89d..01d05868 100644 --- a/addons/mod_loader/internal/path.gd +++ b/addons/mod_loader/internal/path.gd @@ -182,3 +182,24 @@ static func get_path_to_mod_config_file(mod_id: String, config_name: String) -> var mod_config_dir := get_path_to_mod_configs_dir(mod_id) return mod_config_dir.plus_file( config_name + ".json") + + +# Returns the mod directory name ("some-mod") from a given path (e.g. "res://mods-unpacked/some-mod/extensions/extension.gd") +static func get_mod_dir(path: String) -> String: + var initial = ModLoaderStore.UNPACKED_DIR + var ending = "/" + var start_index: int = path.find(initial) + if start_index == -1: + ModLoaderLog.error("Initial string not found.", LOG_NAME) + return "" + + start_index += initial.length() + + var end_index: int = path.find(ending, start_index) + if end_index == -1: + ModLoaderLog.error("Ending string not found.", LOG_NAME) + return "" + + var found_string: String = path.substr(start_index, end_index - start_index) + + return found_string diff --git a/addons/mod_loader/internal/script_extension.gd b/addons/mod_loader/internal/script_extension.gd index 5756a7b8..1874891a 100644 --- a/addons/mod_loader/internal/script_extension.gd +++ b/addons/mod_loader/internal/script_extension.gd @@ -7,6 +7,7 @@ extends Reference const LOG_NAME := "ModLoader:ScriptExtension" + # Sort script extensions by inheritance and apply them in order static func handle_script_extensions() -> void: var extension_paths := [] @@ -15,10 +16,10 @@ static func handle_script_extensions() -> void: extension_paths.push_back(extension_path) else: ModLoaderLog.error("The child script path '%s' does not exist" % [extension_path], LOG_NAME) - + # Sort by inheritance extension_paths.sort_custom(InheritanceSorting.new(), "_check_inheritances") - + # Load and install all extensions for extension in extension_paths: var script: Script = apply_extension(extension) @@ -30,6 +31,11 @@ static func handle_script_extensions() -> void: # a script extending script B if A is an ancestor of B. class InheritanceSorting: var stack_cache := {} + # This dictionary's keys are mod_ids and it stores the corresponding position in the load_order + var load_order := {} + + func _init() -> void: + _populate_load_order_table() # Comparator function. return true if a should go before b. This may # enforce conditions beyond the stated inheritance relationship. @@ -45,11 +51,11 @@ class InheritanceSorting: return a_stack[index] < b_stack[index] last_index = index - if last_index < b_stack.size(): + if last_index < b_stack.size() - 1: return true - return extension_a < extension_b - + return compare_mods_order(extension_a, extension_b) + # Returns a list of scripts representing all the ancestors of the extension # script with the most recent ancestor last. # @@ -57,18 +63,33 @@ class InheritanceSorting: func cached_inheritances_stack(extension_path: String) -> Array: if stack_cache.has(extension_path): return stack_cache[extension_path] - + var stack := [] - + var parent_script: Script = load(extension_path) while parent_script: stack.push_front(parent_script.resource_path) parent_script = parent_script.get_base_script() stack.pop_back() - + stack_cache[extension_path] = stack return stack + # Secondary comparator function for resolving scripts extending the same vanilla script + # Will return whether a comes before b in the load order + func compare_mods_order(extension_a: String, extension_b: String) -> bool: + var mod_a_id: String = _ModLoaderPath.get_mod_dir(extension_a) + var mod_b_id: String = _ModLoaderPath.get_mod_dir(extension_b) + + return load_order[mod_a_id] < load_order[mod_b_id] + + # Populate a load order dictionary for faster access and comparison between mod ids + func _populate_load_order_table() -> void: + var mod_index := 0 + for mod in ModLoaderStore.mod_load_order: + load_order[mod.dir_name] = mod_index + mod_index += 1 + static func apply_extension(extension_path: String) -> Script: # Check path to file exists @@ -76,7 +97,7 @@ static func apply_extension(extension_path: String) -> Script: ModLoaderLog.error("The child script path '%s' does not exist" % [extension_path], LOG_NAME) return null - var child_script: Script = ResourceLoader.load(extension_path) + var child_script: Script = load(extension_path) # Adding metadata that contains the extension script path # We cannot get that path in any other way # Passing the child_script as is would return the base script path @@ -89,8 +110,7 @@ static func apply_extension(extension_path: String) -> Script: # class multiple times. # This is also needed to make Godot instantiate the extended class # when creating singletons. - # The actual instance is thrown away. - child_script.new() + child_script.reload() var parent_script: Script = child_script.get_base_script() var parent_script_path: String = parent_script.resource_path @@ -110,6 +130,7 @@ static func apply_extension(extension_path: String) -> Script: return child_script + # Reload all children classes of the vanilla class we just extended # Calling reload() the children of an extended class seems to allow them to be extended # e.g if B is a child class of A, reloading B after apply an extender of A allows extenders of B to properly extend B, taking A's extender(s) into account diff --git a/addons/mod_loader/mod_loader.gd b/addons/mod_loader/mod_loader.gd index a0233f72..1f668564 100644 --- a/addons/mod_loader/mod_loader.gd +++ b/addons/mod_loader/mod_loader.gd @@ -24,7 +24,7 @@ signal logged(entry) signal current_config_changed(config) # Prefix for this file when using mod_log or dev_log -const LOG_NAME := "ModLoader" +const LOG_NAME := "ModLoader:Loader" # --- DEPRECATED --- # UNPACKED_DIR was moved to ModLoaderStore. @@ -60,7 +60,8 @@ func _init() -> void: return # Load user profiles into ModLoaderStore - var _success_user_profile_load := ModLoaderUserProfile._load() + if ModLoaderUserProfile.is_initialized(): + var _success_user_profile_load := ModLoaderUserProfile._load() _load_mods() diff --git a/addons/mod_loader/mod_loader_store.gd b/addons/mod_loader/mod_loader_store.gd index 54775ec2..e451eef8 100644 --- a/addons/mod_loader/mod_loader_store.gd +++ b/addons/mod_loader/mod_loader_store.gd @@ -12,7 +12,7 @@ extends Node # Most of these settings should never need to change, aside from the DEBUG_* # options (which should be `false` when distributing compiled PCKs) -const MODLOADER_VERSION = "6.2.0" +const MODLOADER_VERSION = "6.3.0" # If true, a complete array of filepaths is stored for each mod. This is # disabled by default because the operation can be very expensive, but may diff --git a/addons/mod_loader/resources/mod_data.gd b/addons/mod_loader/resources/mod_data.gd index c7640e09..5c252f45 100644 --- a/addons/mod_loader/resources/mod_data.gd +++ b/addons/mod_loader/resources/mod_data.gd @@ -88,7 +88,10 @@ func load_configs() -> void: _load_config(config_file_path) # Set the current_config based on the user profile - current_config = ModLoaderConfig.get_current_config(dir_name) + if ModLoaderUserProfile.is_initialized(): + current_config = ModLoaderConfig.get_current_config(dir_name) + else: + current_config = ModLoaderConfig.get_config(dir_name, ModLoaderConfig.DEFAULT_CONFIG_NAME) # Create a new ModConfig instance for each Config JSON and add it to the configs dictionary. diff --git a/addons/mod_loader/resources/mod_user_profile.gd b/addons/mod_loader/resources/mod_user_profile.gd index de485d20..ad08c983 100644 --- a/addons/mod_loader/resources/mod_user_profile.gd +++ b/addons/mod_loader/resources/mod_user_profile.gd @@ -4,8 +4,8 @@ class_name ModUserProfile # This Class is used to represent a User Profile for the ModLoader. -var name := "" -var mod_list := {} +export var name := "" +export var mod_list := {} func _init(_name := "", _mod_list := {}) -> void: diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..1bb44ea0 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,10 @@ +.import/ +*.import +addons/ + +# Gut stuff in root dir +asset_lib_icon.png +gut_panel.png +.gut_editor_shortcuts.cfg +BigFont.tres +BigFontTheme.tres diff --git a/test/.gutconfig.json b/test/.gutconfig.json new file mode 100644 index 00000000..a34105b7 --- /dev/null +++ b/test/.gutconfig.json @@ -0,0 +1,16 @@ +{ + "dirs":["res://Unit/"], + "double_strategy":"partial", + "ignore_pause":false, + "include_subdirs":true, + "inner_class":"", + "log_level":3, + "opacity":100, + "prefix":"test_", + "selected":"", + "should_exit":true, + "should_maximize":true, + "suffix":".gd", + "tests":[], + "unit_test_name":"" +} \ No newline at end of file diff --git a/test/TestRunner.tscn b/test/TestRunner.tscn new file mode 100644 index 00000000..71e6f6b1 --- /dev/null +++ b/test/TestRunner.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/gut/plugin_control.gd" type="Script" id=1] + +[node name="TestRunner" type="Control"] +margin_right = 740.0 +margin_bottom = 250.0 +rect_min_size = Vector2( 740, 250 ) +script = ExtResource( 1 ) +_directory1 = "res://Unit" diff --git a/test/Unit/test_script_extension_sorting.gd b/test/Unit/test_script_extension_sorting.gd new file mode 100644 index 00000000..550c1026 --- /dev/null +++ b/test/Unit/test_script_extension_sorting.gd @@ -0,0 +1,29 @@ +extends GutTest + + +#var order_before_357_correct := [ +# "res://mods-unpacked/test-mod3/extensions/script_extension_sorting/script_b.gd", +# "res://mods-unpacked/test-mod2/extensions/script_extension_sorting/script_c.gd", +# "res://mods-unpacked/test-mod1/extensions/script_extension_sorting/script_c.gd", +# "res://mods-unpacked/test-mod3/extensions/script_extension_sorting/script_d.gd" +#] + +var order_after_357_correct := [ + "res://mods-unpacked/test-mod3/extensions/script_extension_sorting/script_b.gd", + "res://mods-unpacked/test-mod1/extensions/script_extension_sorting/script_c.gd", + "res://mods-unpacked/test-mod2/extensions/script_extension_sorting/script_c.gd", + "res://mods-unpacked/test-mod3/extensions/script_extension_sorting/script_d.gd" +] + + +func test_handle_script_extensions(): + var extension_paths := [ + "res://mods-unpacked/test-mod1/extensions/script_extension_sorting/script_c.gd", + "res://mods-unpacked/test-mod2/extensions/script_extension_sorting/script_c.gd", + "res://mods-unpacked/test-mod3/extensions/script_extension_sorting/script_b.gd", + "res://mods-unpacked/test-mod3/extensions/script_extension_sorting/script_d.gd" + ] + + extension_paths.sort_custom(_ModLoaderScriptExtension.InheritanceSorting.new(), "_check_inheritances") + + assert_true(extension_paths == order_after_357_correct, "Expected %s but was %s instead" % [JSON.print(order_after_357_correct, "\t"), JSON.print(extension_paths, "\t")]) diff --git a/test/default_env.tres b/test/default_env.tres new file mode 100644 index 00000000..20207a4a --- /dev/null +++ b/test/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="ProceduralSky" id=1] + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/test/icon.png b/test/icon.png new file mode 100644 index 00000000..c98fbb60 Binary files /dev/null and b/test/icon.png differ diff --git a/test/mods-unpacked/test-mod1/extensions/script_extension_sorting/script_c.gd b/test/mods-unpacked/test-mod1/extensions/script_extension_sorting/script_c.gd new file mode 100644 index 00000000..b2426863 --- /dev/null +++ b/test/mods-unpacked/test-mod1/extensions/script_extension_sorting/script_c.gd @@ -0,0 +1 @@ +extends "res://script_extension_sorting/script_c.gd" diff --git a/test/mods-unpacked/test-mod1/manifest.json b/test/mods-unpacked/test-mod1/manifest.json new file mode 100644 index 00000000..9c30e49d --- /dev/null +++ b/test/mods-unpacked/test-mod1/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "mod1", + "namespace": "test", + "version_number": "0.0.1", + "description": "Description of your mod...", + "website_url": "https://github.com/exampleauthor/examplemod", + "dependencies": [ + + ], + "extra": { + "godot": { + "authors": [ + "AuthorName" + ], + "optional_dependencies": [ + + ], + "compatible_game_version": [ + "0.0.1" + ], + "compatible_mod_loader_version": [ + "6.1.0" + ], + "incompatibilities": [ + + ], + "load_before": [ + + ], + "tags": [ + + ], + "config_schema": { + + }, + "description_rich": "", + "image": null + } + } +} \ No newline at end of file diff --git a/test/mods-unpacked/test-mod1/mod_main.gd b/test/mods-unpacked/test-mod1/mod_main.gd new file mode 100644 index 00000000..0441f7e1 --- /dev/null +++ b/test/mods-unpacked/test-mod1/mod_main.gd @@ -0,0 +1,33 @@ +extends Node + + +const TEST_MOD1_DIR := "test-mod1" +const TEST_MOD1_LOG_NAME := "test-mod1:Main" + +var mod_dir_path := "" +var extensions_dir_path := "" +var translations_dir_path := "" + + +func _init() -> void: + mod_dir_path = ModLoaderMod.get_unpacked_dir().plus_file(TEST_MOD1_DIR) + # Add extensions + install_script_extensions() + # Add translations + add_translations() + + +func install_script_extensions() -> void: + extensions_dir_path = mod_dir_path.plus_file("extensions") + ModLoaderMod.install_script_extension(extensions_dir_path.plus_file("script_extension_sorting/script_c.gd")) + + + +func add_translations() -> void: + translations_dir_path = mod_dir_path.plus_file("translations") + + +func _ready() -> void: + pass + + diff --git a/test/mods-unpacked/test-mod2/extensions/script_extension_sorting/script_c.gd b/test/mods-unpacked/test-mod2/extensions/script_extension_sorting/script_c.gd new file mode 100644 index 00000000..b2426863 --- /dev/null +++ b/test/mods-unpacked/test-mod2/extensions/script_extension_sorting/script_c.gd @@ -0,0 +1 @@ +extends "res://script_extension_sorting/script_c.gd" diff --git a/test/mods-unpacked/test-mod2/manifest.json b/test/mods-unpacked/test-mod2/manifest.json new file mode 100644 index 00000000..b7ff3179 --- /dev/null +++ b/test/mods-unpacked/test-mod2/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "mod2", + "namespace": "test", + "version_number": "0.0.1", + "description": "Description of your mod...", + "website_url": "https://github.com/exampleauthor/examplemod", + "dependencies": [ + "test-mod1" + ], + "extra": { + "godot": { + "authors": [ + "AuthorName" + ], + "optional_dependencies": [ + + ], + "compatible_game_version": [ + "0.0.1" + ], + "compatible_mod_loader_version": [ + "6.1.0" + ], + "incompatibilities": [ + + ], + "load_before": [ + + ], + "tags": [ + + ], + "config_schema": { + + }, + "description_rich": "", + "image": null + } + } +} \ No newline at end of file diff --git a/test/mods-unpacked/test-mod2/mod_main.gd b/test/mods-unpacked/test-mod2/mod_main.gd new file mode 100644 index 00000000..8313fa65 --- /dev/null +++ b/test/mods-unpacked/test-mod2/mod_main.gd @@ -0,0 +1,33 @@ +extends Node + + +const TEST_MOD2_DIR := "test-mod2" +const TEST_MOD2_LOG_NAME := "test-mod2:Main" + +var mod_dir_path := "" +var extensions_dir_path := "" +var translations_dir_path := "" + + +func _init() -> void: + mod_dir_path = ModLoaderMod.get_unpacked_dir().plus_file(TEST_MOD2_DIR) + # Add extensions + install_script_extensions() + # Add translations + add_translations() + + +func install_script_extensions() -> void: + extensions_dir_path = mod_dir_path.plus_file("extensions") + ModLoaderMod.install_script_extension(extensions_dir_path.plus_file("script_extension_sorting/script_c.gd")) + + + +func add_translations() -> void: + translations_dir_path = mod_dir_path.plus_file("translations") + + +func _ready() -> void: + pass + + diff --git a/test/mods-unpacked/test-mod3/extensions/script_extension_sorting/script_b.gd b/test/mods-unpacked/test-mod3/extensions/script_extension_sorting/script_b.gd new file mode 100644 index 00000000..cf2a4c1f --- /dev/null +++ b/test/mods-unpacked/test-mod3/extensions/script_extension_sorting/script_b.gd @@ -0,0 +1 @@ +extends "res://script_extension_sorting/script_b.gd" diff --git a/test/mods-unpacked/test-mod3/extensions/script_extension_sorting/script_d.gd b/test/mods-unpacked/test-mod3/extensions/script_extension_sorting/script_d.gd new file mode 100644 index 00000000..d8ffca0d --- /dev/null +++ b/test/mods-unpacked/test-mod3/extensions/script_extension_sorting/script_d.gd @@ -0,0 +1 @@ +extends "res://script_extension_sorting/script_d.gd" diff --git a/test/mods-unpacked/test-mod3/manifest.json b/test/mods-unpacked/test-mod3/manifest.json new file mode 100644 index 00000000..8d28ef05 --- /dev/null +++ b/test/mods-unpacked/test-mod3/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "mod3", + "namespace": "test", + "version_number": "0.0.1", + "description": "Description of your mod...", + "website_url": "https://github.com/exampleauthor/examplemod", + "dependencies": [ + + ], + "extra": { + "godot": { + "authors": [ + "AuthorName" + ], + "optional_dependencies": [ + + ], + "compatible_game_version": [ + "0.0.1" + ], + "compatible_mod_loader_version": [ + "6.1.0" + ], + "incompatibilities": [ + + ], + "load_before": [ + + ], + "tags": [ + + ], + "config_schema": { + + }, + "description_rich": "", + "image": null + } + } +} \ No newline at end of file diff --git a/test/mods-unpacked/test-mod3/mod_main.gd b/test/mods-unpacked/test-mod3/mod_main.gd new file mode 100644 index 00000000..98227d8e --- /dev/null +++ b/test/mods-unpacked/test-mod3/mod_main.gd @@ -0,0 +1,33 @@ +extends Node + + +const TEST_MOD3_DIR := "test-mod3" +const TEST_MOD3_LOG_NAME := "test-mod3:Main" + +var mod_dir_path := "" +var extensions_dir_path := "" +var translations_dir_path := "" + + +func _init() -> void: + mod_dir_path = ModLoaderMod.get_unpacked_dir().plus_file(TEST_MOD3_DIR) + # Add extensions + install_script_extensions() + # Add translations + add_translations() + + +func install_script_extensions() -> void: + extensions_dir_path = mod_dir_path.plus_file("extensions") + ModLoaderMod.install_script_extension(extensions_dir_path.plus_file("script_extension_sorting/script_b.gd")) + ModLoaderMod.install_script_extension(extensions_dir_path.plus_file("script_extension_sorting/script_d.gd")) + + +func add_translations() -> void: + translations_dir_path = mod_dir_path.plus_file("translations") + + +func _ready() -> void: + pass + + diff --git a/test/project.godot b/test/project.godot new file mode 100644 index 00000000..a2d7f4b7 --- /dev/null +++ b/test/project.godot @@ -0,0 +1,216 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ { +"base": "Reference", +"class": "GutHookScript", +"language": "GDScript", +"path": "res://addons/gut/hook_script.gd" +}, { +"base": "Node", +"class": "GutTest", +"language": "GDScript", +"path": "res://addons/gut/test.gd" +}, { +"base": "Reference", +"class": "JSONSchema", +"language": "GDScript", +"path": "res://addons/JSON_Schema_Validator/json_schema_validator.gd" +}, { +"base": "Resource", +"class": "ModConfig", +"language": "GDScript", +"path": "res://addons/mod_loader/resources/mod_config.gd" +}, { +"base": "Resource", +"class": "ModData", +"language": "GDScript", +"path": "res://addons/mod_loader/resources/mod_data.gd" +}, { +"base": "Object", +"class": "ModLoaderConfig", +"language": "GDScript", +"path": "res://addons/mod_loader/api/config.gd" +}, { +"base": "Resource", +"class": "ModLoaderCurrentOptions", +"language": "GDScript", +"path": "res://addons/mod_loader/resources/options_current.gd" +}, { +"base": "Node", +"class": "ModLoaderDeprecated", +"language": "GDScript", +"path": "res://addons/mod_loader/api/deprecated.gd" +}, { +"base": "Node", +"class": "ModLoaderLog", +"language": "GDScript", +"path": "res://addons/mod_loader/api/log.gd" +}, { +"base": "Object", +"class": "ModLoaderMod", +"language": "GDScript", +"path": "res://addons/mod_loader/api/mod.gd" +}, { +"base": "Reference", +"class": "ModLoaderModManager", +"language": "GDScript", +"path": "res://addons/mod_loader/api/mod_manager.gd" +}, { +"base": "Resource", +"class": "ModLoaderOptionsProfile", +"language": "GDScript", +"path": "res://addons/mod_loader/resources/options_profile.gd" +}, { +"base": "Reference", +"class": "ModLoaderSetupLog", +"language": "GDScript", +"path": "res://addons/mod_loader/setup/setup_log.gd" +}, { +"base": "Reference", +"class": "ModLoaderSetupUtils", +"language": "GDScript", +"path": "res://addons/mod_loader/setup/setup_utils.gd" +}, { +"base": "Object", +"class": "ModLoaderUserProfile", +"language": "GDScript", +"path": "res://addons/mod_loader/api/profile.gd" +}, { +"base": "Node", +"class": "ModLoaderUtils", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/mod_loader_utils.gd" +}, { +"base": "Resource", +"class": "ModManifest", +"language": "GDScript", +"path": "res://addons/mod_loader/resources/mod_manifest.gd" +}, { +"base": "Resource", +"class": "ModUserProfile", +"language": "GDScript", +"path": "res://addons/mod_loader/resources/mod_user_profile.gd" +}, { +"base": "Node", +"class": "ScriptA", +"language": "GDScript", +"path": "res://script_extension_sorting/script_a.gd" +}, { +"base": "ScriptA", +"class": "ScriptB", +"language": "GDScript", +"path": "res://script_extension_sorting/script_b.gd" +}, { +"base": "ScriptB", +"class": "ScriptC", +"language": "GDScript", +"path": "res://script_extension_sorting/script_c.gd" +}, { +"base": "Node", +"class": "ScriptD", +"language": "GDScript", +"path": "res://script_extension_sorting/script_d.gd" +}, { +"base": "Reference", +"class": "_ModLoaderCLI", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/cli.gd" +}, { +"base": "Reference", +"class": "_ModLoaderCache", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/cache.gd" +}, { +"base": "Reference", +"class": "_ModLoaderDependency", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/dependency.gd" +}, { +"base": "Reference", +"class": "_ModLoaderFile", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/file.gd" +}, { +"base": "Object", +"class": "_ModLoaderGodot", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/godot.gd" +}, { +"base": "Reference", +"class": "_ModLoaderPath", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/path.gd" +}, { +"base": "Reference", +"class": "_ModLoaderScriptExtension", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/script_extension.gd" +}, { +"base": "Node", +"class": "_ModLoaderSteam", +"language": "GDScript", +"path": "res://addons/mod_loader/internal/third_party/steam.gd" +} ] +_global_script_class_icons={ +"GutHookScript": "", +"GutTest": "", +"JSONSchema": "", +"ModConfig": "", +"ModData": "", +"ModLoaderConfig": "", +"ModLoaderCurrentOptions": "", +"ModLoaderDeprecated": "", +"ModLoaderLog": "", +"ModLoaderMod": "", +"ModLoaderModManager": "", +"ModLoaderOptionsProfile": "", +"ModLoaderSetupLog": "", +"ModLoaderSetupUtils": "", +"ModLoaderUserProfile": "", +"ModLoaderUtils": "", +"ModManifest": "", +"ModUserProfile": "", +"ScriptA": "", +"ScriptB": "", +"ScriptC": "", +"ScriptD": "", +"_ModLoaderCLI": "", +"_ModLoaderCache": "", +"_ModLoaderDependency": "", +"_ModLoaderFile": "", +"_ModLoaderGodot": "", +"_ModLoaderPath": "", +"_ModLoaderScriptExtension": "", +"_ModLoaderSteam": "" +} + +[application] + +config/name="Godot Mod Loader Test" +run/main_scene="res://TestRunner.tscn" +config/icon="res://icon.png" + +[autoload] + +ModLoaderStore="*res://addons/mod_loader/mod_loader_store.gd" +ModLoader="*res://addons/mod_loader/mod_loader.gd" + +[gui] + +common/drop_mouse_on_gui_input_disabled=true + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +environment/default_environment="res://default_env.tres" diff --git a/test/run_tests.sh b/test/run_tests.sh new file mode 100644 index 00000000..ce3aa5be --- /dev/null +++ b/test/run_tests.sh @@ -0,0 +1 @@ +$1 -d -s --path $PWD addons/gut/gut_cmdln.gd \ No newline at end of file diff --git a/test/script_extension_sorting/script_a.gd b/test/script_extension_sorting/script_a.gd new file mode 100644 index 00000000..b1c04e1e --- /dev/null +++ b/test/script_extension_sorting/script_a.gd @@ -0,0 +1,2 @@ +class_name ScriptA +extends Node diff --git a/test/script_extension_sorting/script_b.gd b/test/script_extension_sorting/script_b.gd new file mode 100644 index 00000000..ae9663bd --- /dev/null +++ b/test/script_extension_sorting/script_b.gd @@ -0,0 +1,2 @@ +class_name ScriptB +extends ScriptA diff --git a/test/script_extension_sorting/script_c.gd b/test/script_extension_sorting/script_c.gd new file mode 100644 index 00000000..3492002b --- /dev/null +++ b/test/script_extension_sorting/script_c.gd @@ -0,0 +1,2 @@ +class_name ScriptC +extends ScriptB diff --git a/test/script_extension_sorting/script_d.gd b/test/script_extension_sorting/script_d.gd new file mode 100644 index 00000000..ed9e614e --- /dev/null +++ b/test/script_extension_sorting/script_d.gd @@ -0,0 +1,2 @@ +class_name ScriptD +extends Node