Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: ✨ Workshop Support #148

Merged
Merged
Show file tree
Hide file tree
Changes from 11 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
115 changes: 93 additions & 22 deletions addons/mod_loader/mod_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,33 @@ var loaded_vanilla_parents_cache := {}
# Helps to decide whether a script extension should go through the _handle_script_extensions process
var is_initializing := true

# True if ModLoader has displayed the warning about using zipped mods
var has_shown_editor_warning := false

# Keeps track of logged messages, to avoid flooding the log with duplicate notices
var logged_messages := []

# Path to the options resource
# See: res://addons/mod_loader/options/options_current_data.gd
var ml_options_path := "res://addons/mod_loader/options/options_current.tres"

# These variables handle various options, which can be changed via Godot's GUI
# by adding a ModLoaderOptions resource to the resource file specified by
# `ml_options_path`. See res://addons/mod_loader/options_examples for some
# resource files you can add to the options_curent file.
# See: res://addons/mod_loader/options/classes/options_profile.gd
# See: res://addons/mod_loader/options/options_current_data.gd
var ml_options_path := "res://addons/mod_loader/options/options_current.tres"
var ml_options := {
enable_mods = true,
log_level = ModLoaderUtils.verbosity_level.DEBUG,
path_to_mods = "res://mods",
path_to_configs = "res://configs",
use_steam_workshop_path = false,

# If true, ModLoader will load mod ZIPs from the Steam workshop directory,
# instead of the default location (res://mods)
steam_workshop_enabled = false,

# Can be used in the editor to load mods from your Steam workshop directory
steam_workshop_path_override = ""
}


Expand Down Expand Up @@ -253,26 +264,39 @@ func _check_first_autoload() -> void:
# Loop over "res://mods" and add any mod zips to the unpacked virtual directory
# (UNPACKED_DIR)
func _load_mod_zips() -> int:
# Path to the games mod folder
var game_mod_folder_path := ModLoaderUtils.get_local_folder_dir("mods")
if not os_mods_path_override == "":
game_mod_folder_path = os_mods_path_override
var zipped_mods_count := 0

var dir := Directory.new()
if not dir.open(game_mod_folder_path) == OK:
ModLoaderUtils.log_warning("Can't open mod folder %s." % game_mod_folder_path, LOG_NAME)
if not ml_options.steam_workshop_enabled:
# Path to the games mod folder
var mods_folder_path := ModLoaderUtils.get_local_folder_dir("mods")

# If we're not using Steam workshop, just loop over the mod ZIPs.
zipped_mods_count += _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()

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:
ModLoaderUtils.log_error("Can't open mod folder %s (Error: %s)" % [folder_path, mod_dir_open_error], LOG_NAME)
return -1
if not dir.list_dir_begin() == OK:
ModLoaderUtils.log_warning("Can't read mod folder %s." % game_mod_folder_path, LOG_NAME)
var mod_dir_listdir_error := mod_dir.list_dir_begin()
if not mod_dir_listdir_error == OK:
ModLoaderUtils.log_error("Can't read mod folder %s (Error: %s)" % [folder_path, mod_dir_listdir_error], LOG_NAME)
return -1

var has_shown_editor_warning := false

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

# If there is no more file
if mod_zip_file_name == "":
Expand All @@ -284,11 +308,11 @@ func _load_mod_zips() -> int:
continue

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

var mod_folder_path := game_mod_folder_path.plus_file(mod_zip_file_name)
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)

Expand All @@ -311,15 +335,62 @@ func _load_mod_zips() -> int:
# 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
ModLoaderUtils.log_error("%s failed to load." % mod_zip_file_name, LOG_NAME)
ModLoaderUtils.log_error(str(mod_zip_file_name, " failed to load."), LOG_NAME)
ithinkandicode marked this conversation as resolved.
Show resolved Hide resolved
continue

# Mod successfully loaded!
ModLoaderUtils.log_success("%s loaded." % mod_zip_file_name, LOG_NAME)
zipped_mods_count += 1
temp_zipped_mods_count += 1

dir.list_dir_end()
return zipped_mods_count
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 := ModLoaderUtils.get_steam_workshop_dir()

if not ml_options.steam_workshop_path_override == "":
workshop_folder_path = ml_options.steam_workshop_path_override

ModLoaderUtils.log_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:
ModLoaderUtils.log_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:
ModLoaderUtils.log_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

ModLoaderUtils.log_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,
Expand Down
56 changes: 56 additions & 0 deletions addons/mod_loader/mod_loader_utils.gd
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,9 @@ static func get_flat_view_dict(p_dir := "res://", p_match := "", p_match_is_rege
return data


# Saving (Files)
# =============================================================================

# 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
Expand Down Expand Up @@ -529,3 +532,56 @@ static func save_string_to_file(save_string: String, filepath: String) -> bool:
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)


# Steam
ithinkandicode marked this conversation as resolved.
Show resolved Hide resolved
# =============================================================================

# 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).
# For reference, these are the paths of a Steam game and its workshop folder:
# GAME = Steam/steamapps/common/GameName
# WORKSHOP = Steam/steamapps/workshop/content/AppID
# Eg. Brotato:
# GAME = Steam/steamapps/common/Brotato
# WORKSHOP = Steam/steamapps/workshop/content/1942280
static func get_steam_workshop_dir() -> String:
var game_install_directory := get_local_folder_dir()
var path := ""

# Traverse up to the steamapps directory (ie. `cd ..\..\` on Windows)
var path_array := game_install_directory.split("/")
path_array.resize(path_array.size() - 2)

# Reconstruct the path, now that it has "common/GameName" removed
path = "/".join(path_array)

# Append the workgame's workshop path
path = path.plus_file("workshop/content/" + get_steam_app_id())

return path


# Gets the steam app ID from steam_data.json, which should be in the root
# directory (ie. res://steam_data.json). This file is used by Godot Workshop
# 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 := get_local_folder_dir()
var steam_app_id := ""
var file := File.new()

if file.open(game_install_directory.plus_file("steam_data.json"), File.READ) == OK:
var file_content: Dictionary = parse_json(file.get_as_text())
file.close()

if not file_content.has("app_id"):
log_error("The steam_data file does not contain an app ID. Mod uploading will not work.", LOG_NAME)
return ""

steam_app_id = file_content.app_id
else :
log_error("Can't open steam_data file, \"%s\". Please make sure the file exists and is valid." % game_install_directory.plus_file("steam_data.json"), LOG_NAME)

return steam_app_id
3 changes: 2 additions & 1 deletion addons/mod_loader/options/classes/options_profile.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export (bool) var enable_mods = true
export (ModLoaderUtils.verbosity_level) var log_level: = ModLoaderUtils.verbosity_level.DEBUG
export (String, DIR) var path_to_mods = "res://mods"
export (String, DIR) var path_to_configs = "res://configs"
export (bool) var use_steam_workshop_path = false
export (bool) var steam_workshop_enabled = false
export (String, DIR) var steam_workshop_path_override = ""
3 changes: 2 additions & 1 deletion addons/mod_loader/options/profiles/current.tres
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enable_mods = true
log_level = 3
path_to_mods = "res://mods"
path_to_configs = "res://configs"
use_steam_workshop_path = false
steam_workshop_enabled = false
steam_workshop_path_override = ""
3 changes: 2 additions & 1 deletion addons/mod_loader/options/profiles/default.tres
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enable_mods = true
log_level = 3
path_to_mods = "res://mods"
path_to_configs = "res://configs"
use_steam_workshop_path = false
steam_workshop_enabled = false
steam_workshop_path_override = ""
3 changes: 2 additions & 1 deletion addons/mod_loader/options/profiles/disable_mods.tres
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enable_mods = false
log_level = 3
path_to_mods = "res://mods"
path_to_configs = "res://configs"
use_steam_workshop_path = false
steam_workshop_enabled = false
steam_workshop_path_override = ""
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enable_mods = true
log_level = 2
path_to_mods = "res://mods"
path_to_configs = "res://configs"
use_steam_workshop_path = false
steam_workshop_enabled = false
steam_workshop_path_override = ""
3 changes: 2 additions & 1 deletion addons/mod_loader/options/profiles/production_workshop.tres
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enable_mods = true
log_level = 2
path_to_mods = "res://mods"
path_to_configs = "res://configs"
use_steam_workshop_path = true
steam_workshop_enabled = true
steam_workshop_path_override = ""