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
68 changes: 68 additions & 0 deletions addons/mod_loader/internal/file.gd
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,74 @@ static func _get_json_string_as_dict(string: String) -> Dictionary:
return parsed.result


# Load the mod ZIP from the provided directory
static func load_zips_in_folder(folder_path: String) -> int:
var temp_zipped_mods_count := 0

var mod_dir := Directory.new()
var mod_dir_open_error := mod_dir.open(folder_path)
if not mod_dir_open_error == OK:
ModLoaderLog.error("Can't open mod folder %s (Error: %s)" % [folder_path, mod_dir_open_error], LOG_NAME)
return -1
var mod_dir_listdir_error := mod_dir.list_dir_begin()
if not mod_dir_listdir_error == OK:
ModLoaderLog.error("Can't read mod folder %s (Error: %s)" % [folder_path, mod_dir_listdir_error], LOG_NAME)
return -1

# Get all zip folders inside the game mod folder
while true:
# Get the next file in the directory
var mod_zip_file_name := mod_dir.get_next()

# If there is no more file
if mod_zip_file_name == "":
# Stop loading mod zip files
break

# Ignore files that aren't ZIP or PCK
if not mod_zip_file_name.get_extension() == "zip" and not mod_zip_file_name.get_extension() == "pck":
continue

# If the current file is a directory
if mod_dir.current_is_dir():
# Go to the next file
continue

var mod_folder_path := folder_path.plus_file(mod_zip_file_name)
var mod_folder_global_path := ProjectSettings.globalize_path(mod_folder_path)
var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_folder_global_path, false)

# Notifies developer of an issue with Godot, where using `load_resource_pack`
# in the editor WIPES the entire virtual res:// directory the first time you
# use it. This means that unpacked mods are no longer accessible, because they
# no longer exist in the file system. So this warning basically says
# "don't use ZIPs with unpacked mods!"
# https://github.com/godotengine/godot/issues/19815
# https://github.com/godotengine/godot/issues/16798
if OS.has_feature("editor") and not ModLoaderStore.has_shown_editor_zips_warning:
ModLoaderLog.warning(str(
"Loading any resource packs (.zip/.pck) with `load_resource_pack` will WIPE the entire virtual res:// directory. ",
"If you have any unpacked mods in ", _ModLoaderPath.get_unpacked_mods_dir_path(), ", they will not be loaded. ",
"Please unpack your mod ZIPs instead, and add them to ", _ModLoaderPath.get_unpacked_mods_dir_path()), LOG_NAME)
ModLoaderStore.has_shown_editor_zips_warning = true

ModLoaderLog.debug("Found mod ZIP: %s" % mod_folder_global_path, LOG_NAME)

# If there was an error loading the mod zip file
if not is_mod_loaded_successfully:
# Log the error and continue with the next file
ModLoaderLog.error("%s failed to load." % mod_zip_file_name, LOG_NAME)
continue

# Mod successfully loaded!
ModLoaderLog.success("%s loaded." % mod_zip_file_name, LOG_NAME)
temp_zipped_mods_count += 1

mod_dir.list_dir_end()

return temp_zipped_mods_count


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

Expand Down
6 changes: 4 additions & 2 deletions addons/mod_loader/internal/path.gd
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ static func get_path_to_configs() -> String:
static func get_path_to_mod_configs_dir(mod_id: String) -> String:
var mod_config_dir := get_path_to_configs().plus_file(mod_id)

if not _ModLoaderFile.dir_exists(mod_config_dir):
# Can't use _ModLoaderFile because of cyclic dependency.
if not Directory.new().dir_exists(mod_config_dir):
return ""

return mod_config_dir
Expand All @@ -169,7 +170,8 @@ static func get_path_to_mod_config_file(mod_id: String, config_name: String) ->

var mod_config_file_dir := mod_config_dir.plus_file( config_name + ".json")

if not _ModLoaderFile.file_exists(mod_config_file_dir):
# Can't use _ModLoaderFile because of cyclic dependency.
if not Directory.new().file_exists(mod_config_file_dir):
return ""

return mod_config_file_dir
45 changes: 44 additions & 1 deletion addons/mod_loader/internal/third_party/steam.gd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,49 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam"
# Methods related to Steam and the Steam Workshop


# Load mod ZIPs from Steam workshop folders. Uses 2 loops: One for each
# workshop item's folder, with another inside that which loops over the ZIPs
# inside each workshop item's folder
static func load_steam_workshop_zips() -> int:
var temp_zipped_mods_count := 0
var workshop_folder_path := _get_path_to_workshop()

ModLoaderLog.info("Checking workshop items, with path: \"%s\"" % workshop_folder_path, LOG_NAME)

var workshop_dir := Directory.new()
var workshop_dir_open_error := workshop_dir.open(workshop_folder_path)
if not workshop_dir_open_error == OK:
ModLoaderLog.error("Can't open workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_open_error], LOG_NAME)
return -1
var workshop_dir_listdir_error := workshop_dir.list_dir_begin()
if not workshop_dir_listdir_error == OK:
ModLoaderLog.error("Can't read workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_listdir_error], LOG_NAME)
return -1

# Loop 1: Workshop folders
while true:
# Get the next workshop item folder
var item_dir := workshop_dir.get_next()
var item_path := workshop_dir.get_current_dir() + "/" + item_dir

ModLoaderLog.info("Checking workshop item path: \"%s\"" % item_path, LOG_NAME)

# Stop loading mods when there's no more folders
if item_dir == '':
break

# Only check directories
if not workshop_dir.current_is_dir():
continue

# Loop 2: ZIPs inside the workshop folders
temp_zipped_mods_count += _ModLoaderFile.load_zips_in_folder(ProjectSettings.globalize_path(item_path))

workshop_dir.list_dir_end()

return temp_zipped_mods_count


# Get the path to the Steam workshop folder. Only works for Steam games, as it
# traverses directories relative to where a Steam game and its workshop content
# would be installed. Based on code by Blobfish (developer of Brotato).
Expand All @@ -15,7 +58,7 @@ const LOG_NAME := "ModLoader:ThirdParty:Steam"
# Eg. Brotato:
# GAME = Steam/steamapps/common/Brotato
# WORKSHOP = Steam/steamapps/workshop/content/1942280
static func get_path_to_workshop() -> String:
static func _get_path_to_workshop() -> String:
if ModLoaderStore.ml_options.override_path_to_workshop:
return ModLoaderStore.ml_options.override_path_to_workshop

Expand Down
115 changes: 2 additions & 113 deletions addons/mod_loader/mod_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -209,125 +209,14 @@ func _load_mod_zips() -> int:
var mods_folder_path := _ModLoaderPath.get_path_to_mods()

# If we're not using Steam workshop, just loop over the mod ZIPs.
zipped_mods_count += _load_zips_in_folder(mods_folder_path)
zipped_mods_count += _ModLoaderFile.load_zips_in_folder(mods_folder_path)
else:
# If we're using Steam workshop, loop over the workshop item directories
zipped_mods_count += _load_steam_workshop_zips()
zipped_mods_count += _ModLoaderSteam.load_steam_workshop_zips()

return zipped_mods_count


# Load the mod ZIP from the provided directory
func _load_zips_in_folder(folder_path: String) -> int:
var temp_zipped_mods_count := 0

var mod_dir := Directory.new()
var mod_dir_open_error := mod_dir.open(folder_path)
if not mod_dir_open_error == OK:
ModLoaderLog.error("Can't open mod folder %s (Error: %s)" % [folder_path, mod_dir_open_error], LOG_NAME)
return -1
var mod_dir_listdir_error := mod_dir.list_dir_begin()
if not mod_dir_listdir_error == OK:
ModLoaderLog.error("Can't read mod folder %s (Error: %s)" % [folder_path, mod_dir_listdir_error], LOG_NAME)
return -1

# Get all zip folders inside the game mod folder
while true:
# Get the next file in the directory
var mod_zip_file_name := mod_dir.get_next()

# If there is no more file
if mod_zip_file_name == "":
# Stop loading mod zip files
break

# Ignore files that aren't ZIP or PCK
if not mod_zip_file_name.get_extension() == "zip" and not mod_zip_file_name.get_extension() == "pck":
continue

# If the current file is a directory
if mod_dir.current_is_dir():
# Go to the next file
continue

var mod_folder_path := folder_path.plus_file(mod_zip_file_name)
var mod_folder_global_path := ProjectSettings.globalize_path(mod_folder_path)
var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_folder_global_path, false)

# Notifies developer of an issue with Godot, where using `load_resource_pack`
# in the editor WIPES the entire virtual res:// directory the first time you
# use it. This means that unpacked mods are no longer accessible, because they
# no longer exist in the file system. So this warning basically says
# "don't use ZIPs with unpacked mods!"
# https://github.com/godotengine/godot/issues/19815
# https://github.com/godotengine/godot/issues/16798
if OS.has_feature("editor") and not ModLoaderStore.has_shown_editor_zips_warning:
ModLoaderLog.warning(str(
"Loading any resource packs (.zip/.pck) with `load_resource_pack` will WIPE the entire virtual res:// directory. ",
"If you have any unpacked mods in ", _ModLoaderPath.get_unpacked_mods_dir_path(), ", they will not be loaded. ",
"Please unpack your mod ZIPs instead, and add them to ", _ModLoaderPath.get_unpacked_mods_dir_path()), LOG_NAME)
ModLoaderStore.has_shown_editor_zips_warning = true

ModLoaderLog.debug("Found mod ZIP: %s" % mod_folder_global_path, LOG_NAME)

# If there was an error loading the mod zip file
if not is_mod_loaded_successfully:
# Log the error and continue with the next file
ModLoaderLog.error("%s failed to load." % mod_zip_file_name, LOG_NAME)
continue

# Mod successfully loaded!
ModLoaderLog.success("%s loaded." % mod_zip_file_name, LOG_NAME)
temp_zipped_mods_count += 1

mod_dir.list_dir_end()

return temp_zipped_mods_count


# Load mod ZIPs from Steam workshop folders. Uses 2 loops: One for each
# workshop item's folder, with another inside that which loops over the ZIPs
# inside each workshop item's folder
func _load_steam_workshop_zips() -> int:
var temp_zipped_mods_count := 0
var workshop_folder_path := _ModLoaderSteam.get_path_to_workshop()

ModLoaderLog.info("Checking workshop items, with path: \"%s\"" % workshop_folder_path, LOG_NAME)

var workshop_dir := Directory.new()
var workshop_dir_open_error := workshop_dir.open(workshop_folder_path)
if not workshop_dir_open_error == OK:
ModLoaderLog.error("Can't open workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_open_error], LOG_NAME)
return -1
var workshop_dir_listdir_error := workshop_dir.list_dir_begin()
if not workshop_dir_listdir_error == OK:
ModLoaderLog.error("Can't read workshop folder %s (Error: %s)" % [workshop_folder_path, workshop_dir_listdir_error], LOG_NAME)
return -1

# Loop 1: Workshop folders
while true:
# Get the next workshop item folder
var item_dir := workshop_dir.get_next()
var item_path := workshop_dir.get_current_dir() + "/" + item_dir

ModLoaderLog.info("Checking workshop item path: \"%s\"" % item_path, LOG_NAME)

# Stop loading mods when there's no more folders
if item_dir == '':
break

# Only check directories
if not workshop_dir.current_is_dir():
continue

# Loop 2: ZIPs inside the workshop folders
temp_zipped_mods_count += _load_zips_in_folder(ProjectSettings.globalize_path(item_path))

workshop_dir.list_dir_end()

return temp_zipped_mods_count


# Loop over UNPACKED_DIR and triggers `_init_mod_data` for each mod directory,
# which adds their data to mod_data.
func _setup_mods() -> int:
Expand Down