Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions addons/mod_loader/api/config.gd
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ static func save_mod_config_dictionary(mod_id: String, data: Dictionary, update_
data_new = data_original.duplicate(true)
data_new.merge(data, true)

var configs_path := ModLoaderUtils.get_path_to_configs()
var configs_path := _ModLoaderPath.get_path_to_configs()
var json_path := configs_path.plus_file(mod_id + ".json")

return ModLoaderUtils.save_dictionary_to_json_file(data_new, json_path)
return _ModLoaderFile.save_dictionary_to_json_file(data_new, json_path)


# Saves a single settings to a mod's custom config file.
Expand Down
4 changes: 2 additions & 2 deletions addons/mod_loader/api/mod.gd
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ func reload_mods() -> void:
# (but you should only include classes belonging to your mod)
static func register_global_classes_from_array(new_global_classes: Array) -> void:
ModLoaderUtils.register_global_classes_from_array(new_global_classes)
var _savecustom_error: int = ProjectSettings.save_custom(ModLoaderUtils.get_override_path())
var _savecustom_error: int = ProjectSettings.save_custom(_ModLoaderPath.get_override_path())


# Add a translation file, eg "mytranslation.en.translation". The translation
# file should have been created in Godot already: When you import a CSV, such
# a file will be created for you.
static func add_translation_from_resource(resource_path: String) -> void:
if not File.new().file_exists(resource_path):
if not _ModLoaderFile.file_exists(resource_path):
ModLoaderLog.fatal("Tried to load a translation resource from a file that doesn't exist. The invalid path was: %s" % [resource_path], LOG_NAME)
return

Expand Down
4 changes: 2 additions & 2 deletions addons/mod_loader/api/third_party/steam.gd
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ static func get_path_to_workshop() -> String:
if ModLoaderStore.ml_options.override_path_to_workshop:
return ModLoaderStore.ml_options.override_path_to_workshop

var game_install_directory := ModLoaderUtils.get_local_folder_dir()
var game_install_directory := _ModLoaderPath.get_local_folder_dir()
var path := ""

# Traverse up to the steamapps directory (ie. `cd ..\..\` on Windows)
Expand All @@ -40,7 +40,7 @@ static func get_path_to_workshop() -> String:
# Utility (GWU), which was developed by Brotato developer Blobfish:
# https://github.com/thomasgvd/godot-workshop-utility
static func get_steam_app_id() -> String:
var game_install_directory := ModLoaderUtils.get_local_folder_dir()
var game_install_directory := _ModLoaderPath.get_local_folder_dir()
var steam_app_id := ""
var file := File.new()

Expand Down
6 changes: 2 additions & 4 deletions addons/mod_loader/classes/mod_data.gd
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func load_manifest() -> void:

# Load meta data file
var manifest_path := get_required_mod_file_path(required_mod_files.MANIFEST)
var manifest_dict := ModLoaderUtils.get_json_as_dict(manifest_path)
var manifest_dict := _ModLoaderFile.get_json_as_dict(manifest_path)

if USE_EXTENDED_DEBUGLOG:
ModLoaderLog.debug_json_print("%s loaded manifest data -> " % dir_name, manifest_dict, LOG_NAME)
Expand All @@ -82,12 +82,10 @@ func is_mod_dir_name_same_as_id(mod_manifest: ModManifest) -> bool:

# Confirms that all files from [member required_mod_files] exist
func has_required_files() -> bool:
var file_check := File.new()

for required_file in required_mod_files:
var file_path := get_required_mod_file_path(required_mod_files[required_file])

if !file_check.file_exists(file_path):
if !_ModLoaderFile.file_exists(file_path):
ModLoaderLog.fatal("ERROR - %s is missing a required file: %s" % [dir_name, file_path], LOG_NAME)
is_loadable = false
return is_loadable
Expand Down
85 changes: 85 additions & 0 deletions addons/mod_loader/internal/cli.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
class_name _ModLoaderCLI
extends Reference


# This Class provides util functions for working with cli arguments.
# Currently all of the included functions are internal and should only be used by the mod loader itself.

const LOG_NAME := "ModLoader:CLI"


# Check if the provided command line argument was present when launching the game
static func is_running_with_command_line_arg(argument: String) -> bool:
for arg in OS.get_cmdline_args():
if argument == arg.split("=")[0]:
return true

return false


# Get the command line argument value if present when launching the game
static func get_cmd_line_arg_value(argument: String) -> String:
var args := get_fixed_cmdline_args()

for arg_index in args.size():
var arg := args[arg_index] as String

var key := arg.split("=")[0]
if key == argument:
# format: `--arg=value` or `--arg="value"`
if "=" in arg:
var value := arg.trim_prefix(argument + "=")
value = value.trim_prefix('"').trim_suffix('"')
value = value.trim_prefix("'").trim_suffix("'")
return value

# format: `--arg value` or `--arg "value"`
elif arg_index +1 < args.size() and not args[arg_index +1].begins_with("--"):
return args[arg_index + 1]

return ""


static func get_fixed_cmdline_args() -> PoolStringArray:
return fix_godot_cmdline_args_string_space_splitting(OS.get_cmdline_args())


# Reverses a bug in Godot, which splits input strings at spaces even if they are quoted
# e.g. `--arg="some value" --arg-two 'more value'` becomes `[ --arg="some, value", --arg-two, 'more, value' ]`
static func fix_godot_cmdline_args_string_space_splitting(args: PoolStringArray) -> PoolStringArray:
if not OS.has_feature("editor"): # only happens in editor builds
return args
if OS.has_feature("Windows"): # windows is unaffected
return args

var fixed_args := PoolStringArray([])
var fixed_arg := ""
# if we encounter an argument that contains `=` followed by a quote,
# or an argument that starts with a quote, take all following args and
# concatenate them into one, until we find the closing quote
for arg in args:
var arg_string := arg as String
if '="' in arg_string or '="' in fixed_arg or \
arg_string.begins_with('"') or fixed_arg.begins_with('"'):
if not fixed_arg == "":
fixed_arg += " "
fixed_arg += arg_string
if arg_string.ends_with('"'):
fixed_args.append(fixed_arg.trim_prefix(" "))
fixed_arg = ""
continue
# same thing for single quotes
elif "='" in arg_string or "='" in fixed_arg \
or arg_string.begins_with("'") or fixed_arg.begins_with("'"):
if not fixed_arg == "":
fixed_arg += " "
fixed_arg += arg_string
if arg_string.ends_with("'"):
fixed_args.append(fixed_arg.trim_prefix(" "))
fixed_arg = ""
continue

else:
fixed_args.append(arg_string)

return fixed_args
109 changes: 109 additions & 0 deletions addons/mod_loader/internal/file.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
class_name _ModLoaderFile
extends Reference


# This Class provides util functions for working with files.
# Currently all of the included functions are internal and should only be used by the mod loader itself.

const LOG_NAME := "ModLoader:File"


# Get Data
# =============================================================================

# Parses JSON from a given file path and returns a [Dictionary].
# Returns an empty [Dictionary] if no file exists (check with size() < 1)
static func get_json_as_dict(path: String) -> Dictionary:
var file := File.new()

if !file.file_exists(path):
file.close()
return {}

var error = file.open(path, File.READ)
if not error == OK:
ModLoaderLog.error("Error opening file. Code: %s" % error, LOG_NAME)

var content := file.get_as_text()
return get_json_string_as_dict(content)


# Parses JSON from a given [String] and returns a [Dictionary].
# Returns an empty [Dictionary] on error (check with size() < 1)
static func get_json_string_as_dict(string: String) -> Dictionary:
if string == "":
return {}
var parsed := JSON.parse(string)
if parsed.error:
ModLoaderLog.error("Error parsing JSON", LOG_NAME)
return {}
if not parsed.result is Dictionary:
ModLoaderLog.error("JSON is not a dictionary", LOG_NAME)
return {}
return parsed.result


# Save Data
# =============================================================================

# Saves a dictionary to a file, as a JSON string
static func save_string_to_file(save_string: String, filepath: String) -> bool:
# Create directory if it doesn't exist yet
var file_directory := filepath.get_base_dir()
var dir := Directory.new()

_code_note(str(
"View error codes here:",
"https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-error"
))

if not dir.dir_exists(file_directory):
var makedir_error = dir.make_dir_recursive(file_directory)
if not makedir_error == OK:
ModLoaderLog.fatal("Encountered an error (%s) when attempting to create a directory, with the path: %s" % [makedir_error, file_directory], LOG_NAME)
return false

var file = File.new()

# Save data to the file
var fileopen_error = file.open(filepath, File.WRITE)

if not fileopen_error == OK:
ModLoaderLog.fatal("Encountered an error (%s) when attempting to write to a file, with the path: %s" % [fileopen_error, filepath], LOG_NAME)
return false

file.store_string(save_string)
file.close()

return true


# Saves a dictionary to a file, as a JSON string
static func save_dictionary_to_json_file(data: Dictionary, filepath: String) -> bool:
var json_string = JSON.print(data, "\t")
return save_string_to_file(json_string, filepath)


# Checks
# =============================================================================

static func file_exists(path: String) -> bool:
var file = File.new()
return file.file_exists(path)


static func dir_exists(path: String) -> bool:
var dir = Directory.new()
return dir.dir_exists(path)


# Internal util functions
# =============================================================================
# This are duplicates of the functions in mod_loader_utils.gd to prevent
# a cyclic reference error.

# This is a dummy func. It is exclusively used to show notes in the code that
# stay visible after decompiling a PCK, as is primarily intended to assist new
# modders in understanding and troubleshooting issues.
static func _code_note(_msg:String):
pass
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
class_name ModLoaderGodot
class_name _ModLoaderGodot
extends Object

# API methods for interacting with Godot

# This Class provides methods for interacting with Godot.
# Currently all of the included methods are internal and should only be used by the mod loader itself.

const LOG_NAME := "ModLoader:Godot"

Expand All @@ -10,7 +12,7 @@ const LOG_NAME := "ModLoader:Godot"
# Returns a bool if the position does not match.
# Optionally triggers a fatal error
static func check_autoload_position(autoload_name: String, position_index: int, trigger_error: bool = false) -> bool:
var autoload_array := ModLoaderUtils.get_autoload_array()
var autoload_array := _get_autoload_array()
var autoload_index := autoload_array.find(autoload_name)
var position_matches := autoload_index == position_index

Expand All @@ -24,3 +26,16 @@ static func check_autoload_position(autoload_name: String, position_index: int,
ModLoaderLog.fatal(error_msg + help_msg, LOG_NAME)

return position_matches


# Get an array of all autoloads -> ["autoload/AutoloadName", ...]
static func _get_autoload_array() -> Array:
var autoloads := []

# Get all autoload settings
for prop in ProjectSettings.get_property_list():
var name: String = prop.name
if name.begins_with("autoload/"):
autoloads.append(name.trim_prefix("autoload/"))

return autoloads
Loading