Skip to content

Commit

Permalink
Workshop Support (#148)
Browse files Browse the repository at this point in the history
* Workshop Support: Adds code based on work by Blobfish for Brotato
* Workshop Support: Disable `use_workshop` by default & make the var local to ModLoader
* Workshop Support: Amends from PR review
* Workshop Support: Amends from PR review cont. (type safety)
* Workshop Support: Amends from PR review cont. (type safety) [2]
* Workshop Support: Rename workshop vars to include `steam`
* Workshop Support: Update more older code that was updated since 4.0
* Workshop Support: Lints
* Workshop Support: Integrate ML Options
* Workshop Support: Lints [2]
  • Loading branch information
ithinkandicode authored Mar 1, 2023
1 parent 01983b7 commit b4d31bc
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 27 deletions.
113 changes: 92 additions & 21 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 Down Expand Up @@ -316,10 +340,57 @@ func _load_mod_zips() -> int:

# 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
# =============================================================================

# 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 = ""

0 comments on commit b4d31bc

Please # to comment.