Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Move other utils #63

Merged
merged 6 commits into from
Jan 19, 2023
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
31 changes: 6 additions & 25 deletions addons/mod_loader/mod_data.gd
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ func load_manifest() -> void:
ModLoaderUtils.log_info("Loading mod_manifest (manifest.json) for -> %s" % dir_name, LOG_NAME)

# Load meta data file
var manifest_path = get_required_mod_file_path(required_mod_files.MANIFEST)
var manifest_dict = _get_json_as_dict(manifest_path) # todo get from utils
var manifest_path := get_required_mod_file_path(required_mod_files.MANIFEST)
var manifest_dict := ModLoaderUtils.get_json_as_dict(manifest_path)

ModLoaderUtils.log_info("%s loaded manifest data -> %s" % [dir_name, manifest_dict], LOG_NAME)

Expand All @@ -59,8 +59,8 @@ func load_manifest() -> void:

# Validates if [member dir_name] matches [method ModManifest.get_mod_id]
func is_mod_dir_name_same_as_id() -> bool:
var manifest_id = manifest.get_mod_id()
if dir_name != manifest_id:
var manifest_id := manifest.get_mod_id()
if not dir_name == manifest_id:
ModLoaderUtils.log_fatal('Mod directory name "%s" does not match the data in manifest.json. Expected "%s"' % [ dir_name, manifest_id ], LOG_NAME)
is_loadable = false
return false
Expand All @@ -69,10 +69,10 @@ func is_mod_dir_name_same_as_id() -> bool:

# Confirms that all files from [member required_mod_files] exist
func has_required_files() -> bool:
var file_check = File.new()
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])
var file_path := get_required_mod_file_path(required_mod_files[required_file])

if !file_check.file_exists(file_path):
ModLoaderUtils.log_fatal("ERROR - %s is missing a required file: %s" % [dir_name, file_path], LOG_NAME)
Expand All @@ -95,25 +95,6 @@ func get_required_mod_file_path(required_file: int) -> String:
return ""


# 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: # todo move to utils
var file = File.new()

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

file.open(path, File.READ)
var content = file.get_as_text()

var parsed := JSON.parse(content)
if parsed.error:
# log error
return {}
return parsed.result


#func _to_string() -> String:
# todo if we want it pretty printed

Expand Down
125 changes: 9 additions & 116 deletions addons/mod_loader/mod_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,20 @@ var _saved_objects = []

func _init():
# if mods are not enabled - don't load mods
if REQUIRE_CMD_LINE && (!_check_cmd_line_arg("--enable-mods")):
if REQUIRE_CMD_LINE and not ModLoaderUtils.is_running_with_command_line_arg("--enable-mods"):
return

# Log game install dir
ModLoaderUtils.log_info(str("game_install_directory: ", _get_local_folder_dir()), LOG_NAME)
ModLoaderUtils.log_info("game_install_directory: %s" % ModLoaderUtils.get_local_folder_dir(), LOG_NAME)

# check if we want to use a different mods path that is provided as a command line argument
var cmd_line_mod_path = _get_cmd_line_arg("--mods-path")
var cmd_line_mod_path := ModLoaderUtils.get_cmd_line_arg_value("--mods-path")
if cmd_line_mod_path != "":
os_mods_path_override = cmd_line_mod_path
ModLoaderUtils.log_info("The path mods are loaded from has been changed via the CLI arg `--mods-path`, to: " + cmd_line_mod_path, LOG_NAME)

# Check for the CLI arg that overrides the configs path
var cmd_line_configs_path = _get_cmd_line_arg("--configs-path")
var cmd_line_configs_path = ModLoaderUtils.get_cmd_line_arg_value("--configs-path")
if cmd_line_configs_path != "":
os_configs_path_override = cmd_line_configs_path
ModLoaderUtils.log_info("The path configs are loaded from has been changed via the CLI arg `--configs-path`, to: " + cmd_line_configs_path, LOG_NAME)
Expand Down Expand Up @@ -154,7 +154,7 @@ func _init():
# (UNPACKED_DIR)
func _load_mod_zips():
# Path to the games mod folder
var game_mod_folder_path = _get_local_folder_dir("mods")
var game_mod_folder_path = ModLoaderUtils.get_local_folder_dir("mods")

var dir = Directory.new()
if dir.open(game_mod_folder_path) != OK:
Expand Down Expand Up @@ -257,7 +257,7 @@ func _setup_mods():
# Load mod config JSONs from res://configs
func _load_mod_configs():
var found_configs_count = 0
var configs_path = _get_local_folder_dir("configs")
var configs_path = ModLoaderUtils.get_local_folder_dir("configs")

# CLI override, set with `--configs-path="C://path/configs"`
# (similar to os_mods_path_override)
Expand All @@ -266,7 +266,7 @@ func _load_mod_configs():

for dir_name in mod_data:
var json_path = configs_path.plus_file(dir_name + ".json")
var mod_config = ModData._get_json_as_dict(json_path)
var mod_config = ModLoaderUtils.get_json_as_dict(json_path)

ModLoaderUtils.log_debug(str("Config JSON: Looking for config at path: ", json_path), LOG_NAME)

Expand Down Expand Up @@ -306,7 +306,7 @@ func _load_mod_configs():
# which depends on the name used in a given mod ZIP (eg "mods-unpacked/Folder-Name")
func _init_mod_data(mod_folder_path):
# The file name should be a valid mod id
var dir_name = _get_file_name(mod_folder_path, false, true)
var dir_name = ModLoaderUtils.get_file_name_from_path(mod_folder_path, false, true)

# Path to the mod in UNPACKED_DIR (eg "res://mods-unpacked/My-Mod")
var local_mod_path = str(UNPACKED_DIR, dir_name)
Expand All @@ -320,7 +320,7 @@ func _init_mod_data(mod_folder_path):
# operation if a mod has a large number of files (eg. Brotato's Invasion mod,
# which has ~1,000 files). That's why it's disabled by default
if DEBUG_ENABLE_STORING_FILEPATHS:
mod.file_paths = _get_flat_view_dict(local_mod_path)
mod.file_paths = ModLoaderUtils.get_flat_view_dict(local_mod_path)


# Run dependency checks on a mod, checking any dependencies it lists in its
Expand Down Expand Up @@ -403,113 +403,6 @@ func _init_mod(mod: ModData):
add_child(mod_main_instance, true)


# Utils (Mod Loader)
# =============================================================================

# Util functions used in the mod loading process

# Check if the provided command line argument was present when launching the game
func _check_cmd_line_arg(argument) -> bool:
for arg in OS.get_cmdline_args():
if arg == argument:
return true

return false

# Get the command line argument value if present when launching the game
func _get_cmd_line_arg(argument) -> String:
for arg in OS.get_cmdline_args():
if arg.find("=") > -1:
var key_value = arg.split("=")
# True if the checked argument matches a user-specified arg key
# (eg. checking `--mods-path` will match with `--mods-path="C://mods"`
if key_value[0] == argument:
return key_value[1]

return ""

# Get the path to a local folder. Primarily used to get the (packed) mods
# folder, ie "res://mods" or the OS's equivalent, as well as the configs path
func _get_local_folder_dir(subfolder:String = ""):
var game_install_directory = OS.get_executable_path().get_base_dir()

if OS.get_name() == "OSX":
game_install_directory = game_install_directory.get_base_dir().get_base_dir()

# Fix for running the game through the Godot editor (as the EXE path would be
# the editor's own EXE, which won't have any mod ZIPs)
# if OS.is_debug_build():
if OS.has_feature("editor"):
game_install_directory = "res://"

return game_install_directory.plus_file(subfolder)


func _get_file_name(path, is_lower_case = true, is_no_extension = false):
var file_name = path.get_file()

if(is_lower_case):
file_name = file_name.to_lower()

if(is_no_extension):
var file_extension = file_name.get_extension()
file_name = file_name.replace(str(".",file_extension), '')

return file_name


# Get a flat array of all files in the target directory. This was needed in the
# original version of this script, before becoming deprecated. It may still be
# used if DEBUG_ENABLE_STORING_FILEPATHS is true.
# Source: https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e
func _get_flat_view_dict(p_dir = "res://", p_match = "", p_match_is_regex = false):
var regex = null
if p_match_is_regex:
regex = RegEx.new()
regex.compile(p_match)
if not regex.is_valid():
return []

var dirs = [p_dir]
var first = true
var data = []
while not dirs.empty():
var dir = Directory.new()
var dir_name = dirs.back()
dirs.pop_back()

if dir.open(dir_name) == OK:
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
if not dir_name == "res://":
first = false
# ignore hidden, temporary, or system content
if not file_name.begins_with(".") and not file_name.get_extension() in ["tmp", "import"]:
# If a directory, then add to list of directories to visit
if dir.current_is_dir():
dirs.push_back(dir.get_current_dir() + "/" + file_name)
# If a file, check if we already have a record for the same name
else:
var path = dir.get_current_dir() + ("/" if not first else "") + file_name
# grab all
if not p_match:
data.append(path)
# grab matching strings
elif not p_match_is_regex and file_name.find(p_match, 0) != -1:
data.append(path)
# grab matching regex
else:
var regex_match = regex.search(path)
if regex_match != null:
data.append(path)
# Move on to the next file in this directory
file_name = dir.get_next()
# We've exhausted all files in this directory. Close the iterator.
dir.list_dir_end()
return data


# Helpers
# =============================================================================

Expand Down
112 changes: 108 additions & 4 deletions addons/mod_loader/mod_loader_utils.gd
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
extends Node
class_name ModLoaderUtils

const MOD_LOG_PATH = "user://mods.log"
const LOG_NAME := "ModLoader:ModLoaderUtils"
const MOD_LOG_PATH := "user://mods.log"

enum verbosity_level {
ERROR,
Expand Down Expand Up @@ -84,7 +85,7 @@ static func _loader_log(message: String, mod_name: String, log_type: String = "i


static func _write_to_log_file(log_entry: String) -> void:
var log_file = File.new()
var log_file := File.new()

if not log_file.file_exists(MOD_LOG_PATH):
log_file.open(MOD_LOG_PATH, File.WRITE)
Expand Down Expand Up @@ -127,8 +128,8 @@ static func is_running_with_command_line_arg(argument: String) -> bool:
# Get the command line argument value if present when launching the game
static func get_cmd_line_arg_value(argument: String) -> String:
for arg in OS.get_cmdline_args():
if arg.find("=") > -1:
var key_value = arg.split("=")
if (arg as String).find("=") > -1:
var key_value := (arg as String).split("=")
# True if the checked argument matches a user-specified arg key
# (eg. checking `--mods-path` will match with `--mods-path="C://mods"`
if key_value[0] == argument:
Expand All @@ -147,3 +148,106 @@ static func get_date_time_string() -> String:
]


# Get the path to a local folder. Primarily used to get the (packed) mods
# folder, ie "res://mods" or the OS's equivalent, as well as the configs path
static func get_local_folder_dir(subfolder: String = "") -> String:
var game_install_directory := OS.get_executable_path().get_base_dir()

if OS.get_name() == "OSX":
game_install_directory = game_install_directory.get_base_dir().get_base_dir()

# Fix for running the game through the Godot editor (as the EXE path would be
# the editor's own EXE, which won't have any mod ZIPs)
# if OS.is_debug_build():
if OS.has_feature("editor"):
game_install_directory = "res://"

return game_install_directory.plus_file(subfolder)


# Provide a path, get the file name at the end of the path
static func get_file_name_from_path(path: String, make_lower_case := true, remove_extension := false) -> String:
var file_name := path.get_file()

if make_lower_case:
file_name = file_name.to_lower()

if remove_extension:
file_name = file_name.trim_suffix("." + file_name.get_extension())

return file_name


# 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 {}

file.open(path, File.READ)
var content := file.get_as_text()

var parsed := JSON.parse(content)
if parsed.error:
log_error("Error parsing JSON", LOG_NAME)
return {}
if not parsed.result is Dictionary:
log_error("JSON is not a dictionary", LOG_NAME)
return {}
return parsed.result


# Get a flat array of all files in the target directory. This was needed in the
# original version of this script, before becoming deprecated. It may still be
# used if DEBUG_ENABLE_STORING_FILEPATHS is true.
# Source: https://gist.github.com/willnationsdev/00d97aa8339138fd7ef0d6bd42748f6e
static func get_flat_view_dict(p_dir := "res://", p_match := "", p_match_is_regex := false) -> Array:
var regex: RegEx
if p_match_is_regex:
regex = RegEx.new()
regex.compile(p_match)
if not regex.is_valid():
return []

var dirs := [p_dir]
var first := true
var data := []
while not dirs.empty():
var dir := Directory.new()
var dir_name: String = dirs.back()
dirs.pop_back()

if dir.open(dir_name) == OK:
dir.list_dir_begin()
var file_name := dir.get_next()
while file_name != "":
if not dir_name == "res://":
first = false
# ignore hidden, temporary, or system content
if not file_name.begins_with(".") and not file_name.get_extension() in ["tmp", "import"]:
# If a directory, then add to list of directories to visit
if dir.current_is_dir():
dirs.push_back(dir.get_current_dir().plus_file(file_name))
# If a file, check if we already have a record for the same name
else:
var path := dir.get_current_dir() + ("/" if not first else "") + file_name
# grab all
if not p_match:
data.append(path)
# grab matching strings
elif not p_match_is_regex and file_name.find(p_match, 0) != -1:
data.append(path)
# grab matching regex
else:
var regex_match := regex.search(path)
if regex_match != null:
data.append(path)
# Move on to the next file in this directory
file_name = dir.get_next()
# We've exhausted all files in this directory. Close the iterator.
dir.list_dir_end()
return data