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
2 changes: 1 addition & 1 deletion addons/mod_loader/_export_plugin/export_plugin.gd
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
extends EditorExportPlugin


static var hook_pre_processor: _ModLoaderModHookPreProcessor
var hook_pre_processor: _ModLoaderModHookPreProcessor

func _get_name() -> String:
return "Godot Mod Loader Export Plugin"
Expand Down
6 changes: 4 additions & 2 deletions addons/mod_loader/api/config.gd
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ static func create_config(mod_id: String, config_name: String, config_data: Dict
)

# Check if the mod_config is valid
if not mod_config.is_valid:
#if not mod_config.is_valid:
if not mod_config.is_valid():
return null

# Store the mod_config in the mod's ModData
Expand Down Expand Up @@ -86,7 +87,8 @@ static func update_config(config: ModConfig) -> ModConfig:
return null

# Check if the config passed validation
if not config.is_valid:
#if not config.is_valid:
if not config.is_valid():
ModLoaderLog.error("Update for config \"%s\" failed validation with error message \"%s\"" % [config.name, error_message], LOG_NAME)
return null

Expand Down
172 changes: 116 additions & 56 deletions addons/mod_loader/api/log.gd
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,15 @@ enum VERBOSITY_LEVEL {
DEBUG, ## For debugging, can get quite verbose
}

## Keeps track of logged messages, to avoid flooding the log with duplicate notices
## Can also be used by mods, eg. to create an in-game developer console that
## shows messages
static var logged_messages := {
"all": {},
"by_mod": {},
"by_type": {
"fatal-error": {},
"error": {},
"warning": {},
"info": {},
"success": {},
"debug": {},
"hint": {},
}
enum VERBOSITY_COLOR {
ERROR, ## For errors and fatal errors
WARNING, ## For warnings
INFO, ## For everything informational
SUCCESS,
DEBUG, ## For debugging, can get quite verbose
hint,
}

## Verbosity/Logging level.
## Used to filter out messages below the set level
## (if the [enum VERBOSITY_LEVEL] int of a new entry is larger than the [member verbosity] it is ignored)
static var verbosity: VERBOSITY_LEVEL = VERBOSITY_LEVEL.DEBUG

## Array of mods that should be ignored when logging messages (contains mod IDs as strings)
static var ignored_mods: Array[String] = []

## Highlighting color for hint type log messages
static var hint_color := Color("#70bafa")

## This Sub-Class represents a log entry in ModLoader.
class ModLoaderLogEntry:
extends Resource
Expand Down Expand Up @@ -99,9 +80,15 @@ class ModLoaderLogEntry:

## Get the prefix string for the log entry, including the log type and mod name.[br]
## [br]
## [b]Parameters:[/b][br]
## [param exclude_type] ([bool]): (Optional) If true, the log type (e.g., DEBUG, WARN) will be excluded from the prefix. Default is false.[br]
## [br]
## [b]Returns:[/b] [String]
func get_prefix() -> String:
return "%s %s: " % [type.to_upper(), mod_name]
func get_prefix(exclude_type := false) -> String:
return "%s%s: " % [
"" if exclude_type else "%s " % type.to_upper(),
mod_name
]


## Generate an MD5 hash of the log entry (prefix + message).[br]
Expand Down Expand Up @@ -323,8 +310,8 @@ static func get_all() -> Array:
var log_entries := []

# Get all log entries
for entry_key in logged_messages.all.keys():
var entry: ModLoaderLogEntry = logged_messages.all[entry_key]
for entry_key in ModLoaderStore.logged_messages.all.keys():
var entry: ModLoaderLogEntry = ModLoaderStore.logged_messages.all[entry_key]
log_entries.append_array(entry.get_all_entries())

# Sort them by time
Expand All @@ -343,12 +330,12 @@ static func get_all() -> Array:
static func get_by_mod(mod_name: String) -> Array:
var log_entries := []

if not logged_messages.by_mod.has(mod_name):
if not ModLoaderStore.logged_messages.by_mod.has(mod_name):
error("\"%s\" not found in logged messages." % mod_name, _LOG_NAME)
return []

for entry_key in logged_messages.by_mod[mod_name].keys():
var entry: ModLoaderLogEntry = logged_messages.by_mod[mod_name][entry_key]
for entry_key in ModLoaderStore.logged_messages.by_mod[mod_name].keys():
var entry: ModLoaderLogEntry = ModLoaderStore.logged_messages.by_mod[mod_name][entry_key]
log_entries.append_array(entry.get_all_entries())

return log_entries
Expand All @@ -364,8 +351,8 @@ static func get_by_mod(mod_name: String) -> Array:
static func get_by_type(type: String) -> Array:
var log_entries := []

for entry_key in logged_messages.by_type[type].keys():
var entry: ModLoaderLogEntry = logged_messages.by_type[type][entry_key]
for entry_key in ModLoaderStore.logged_messages.by_type[type].keys():
var entry: ModLoaderLogEntry = ModLoaderStore.logged_messages.by_type[type][entry_key]
log_entries.append_array(entry.get_all_entries())

return log_entries
Expand All @@ -391,6 +378,17 @@ static func get_all_entries_as_string(log_entries: Array) -> Array:
# Internal log functions
# =============================================================================

static func _print_rich(prefix: String, message: String, color: Color, bold := true) -> void:
if OS.has_feature("editor"):
var prefix_text := "[b]%s[/b]" % prefix if bold else prefix
print_rich("[color=%s]%s[/color]%s" % [
color.to_html(false),
prefix_text,
message
])
else:
print(prefix + message)

static func _log(message: String, mod_name: String, log_type: String = "info", only_once := false) -> void:
if _is_mod_name_ignored(mod_name):
return
Expand Down Expand Up @@ -422,63 +420,125 @@ static func _log(message: String, mod_name: String, log_type: String = "info", o
_write_to_log_file(JSON.stringify(get_stack(), " "))
assert(false, message)
"error":
printerr(log_entry.get_prefix() + message)
if ModLoaderStore.has_feature.editor:
printerr(log_entry.get_prefix(true) + message)
else:
printerr(log_entry.get_prefix() + message)
push_error(message)
_write_to_log_file(log_entry.get_entry())
"warning":
if verbosity >= VERBOSITY_LEVEL.WARNING:
print(log_entry.get_prefix() + message)
if _get_verbosity() >= VERBOSITY_LEVEL.WARNING:
_print_rich(
log_entry.get_prefix(),
message,
_get_color(VERBOSITY_COLOR.WARNING)
)
push_warning(message)
_write_to_log_file(log_entry.get_entry())
"info", "success":
if verbosity >= VERBOSITY_LEVEL.INFO:
print(log_entry.get_prefix() + message)
"success":
if _get_verbosity() >= VERBOSITY_LEVEL.INFO:
_print_rich(
log_entry.get_prefix(),
message,
_get_color(VERBOSITY_COLOR.SUCCESS)
)
_write_to_log_file(log_entry.get_entry())
"info":
if _get_verbosity() >= VERBOSITY_LEVEL.INFO:
_print_rich(
log_entry.get_prefix(),
message,
_get_color(VERBOSITY_COLOR.INFO)
)
_write_to_log_file(log_entry.get_entry())
"debug":
if verbosity >= VERBOSITY_LEVEL.DEBUG:
print(log_entry.get_prefix() + message)
if _get_verbosity() >= VERBOSITY_LEVEL.DEBUG:
_print_rich(
log_entry.get_prefix(),
message,
_get_color(VERBOSITY_COLOR.DEBUG),
true if not ModLoaderStore else ModLoaderStore.ml_options.debug_bold
)
_write_to_log_file(log_entry.get_entry())
"hint":
if OS.has_feature("editor") and verbosity >= VERBOSITY_LEVEL.DEBUG:
print_rich("[color=%s]%s[/color]" % [hint_color.to_html(false), log_entry.get_prefix() + message])
if ModLoaderStore.has_feature.editor:
if _get_verbosity() >= VERBOSITY_LEVEL.DEBUG:
_print_rich(
log_entry.get_prefix(),
message,
_get_color(VERBOSITY_COLOR.hint)
)


static func _is_mod_name_ignored(mod_log_name: String) -> bool:
if not ModLoaderStore:
return false

var ignored_mod_log_names := ModLoaderStore.ml_options.ignored_mod_names_in_log as Array

static func _is_mod_name_ignored(mod_name: String) -> bool:
if ignored_mods.is_empty():
# No ignored mod names
if ignored_mod_log_names.size() == 0:
return false

if mod_name in ignored_mods:
# Directly match a full mod log name. ex: "ModLoader:Deprecated"
if mod_log_name in ignored_mod_log_names:
return true

# Match a mod log name with a wildcard. ex: "ModLoader:*"
for ignored_mod_name in ignored_mod_log_names:
if ignored_mod_name.ends_with("*"):
if mod_log_name.begins_with(ignored_mod_name.trim_suffix("*")):
return true

# No match
return false

static func _get_color(verbosity: VERBOSITY_COLOR) -> Color:
if not ModLoaderStore:
return Color("#d4d4d4")

var color = ModLoaderStore.ml_options.get(
"%s_color" % VERBOSITY_COLOR.keys()[verbosity].to_lower()
)
if color == null:
return Color("#d4d4d4")

return color

static func _get_verbosity() -> int:
if not ModLoaderStore:
return VERBOSITY_LEVEL.DEBUG
return ModLoaderStore.ml_options.log_level

static func _store_log(log_entry: ModLoaderLogEntry) -> void:
# HACK: this makes logs from ModLoaderStore unable to be stored
if not ModLoaderStore:
return
var existing_entry: ModLoaderLogEntry

# Store in all
# If it's a new entry
if not logged_messages.all.has(log_entry.get_md5()):
logged_messages.all[log_entry.get_md5()] = log_entry
if not ModLoaderStore.logged_messages.all.has(log_entry.get_md5()):
ModLoaderStore.logged_messages.all[log_entry.get_md5()] = log_entry
# If it's a existing entry
else:
existing_entry = logged_messages.all[log_entry.get_md5()]
existing_entry = ModLoaderStore.logged_messages.all[log_entry.get_md5()]
existing_entry.time = log_entry.time
existing_entry.stack.push_back(log_entry)

# Store in by_mod
# If the mod is not yet in "by_mod" init the entry
if not logged_messages.by_mod.has(log_entry.mod_name):
logged_messages.by_mod[log_entry.mod_name] = {}
if not ModLoaderStore.logged_messages.by_mod.has(log_entry.mod_name):
ModLoaderStore.logged_messages.by_mod[log_entry.mod_name] = {}

logged_messages.by_mod[log_entry.mod_name][log_entry.get_md5()] = log_entry if not existing_entry else existing_entry
ModLoaderStore.logged_messages.by_mod[log_entry.mod_name][log_entry.get_md5()] = log_entry if not existing_entry else existing_entry

# Store in by_type
logged_messages.by_type[log_entry.type.to_lower()][log_entry.get_md5()] = log_entry if not existing_entry else existing_entry
ModLoaderStore.logged_messages.by_type[log_entry.type.to_lower()][log_entry.get_md5()] = log_entry if not existing_entry else existing_entry


static func _is_logged_before(entry: ModLoaderLogEntry) -> bool:
if not logged_messages.all.has(entry.get_md5()):
if not ModLoaderStore.logged_messages.all.has(entry.get_md5()):
return false

return true
Expand Down
3 changes: 3 additions & 0 deletions addons/mod_loader/api/mod.gd
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const LOG_NAME := "ModLoader:Mod"
static func install_script_extension(child_script_path: String) -> void:
var mod_id: String = _ModLoaderPath.get_mod_dir(child_script_path)
var mod_data: ModData = get_mod_data(mod_id)
if mod_data == null:
ModLoaderLog.warning('"%s" is not a valid mod id! Please ensure the supplied path is valid!' % mod_id, LOG_NAME)

if not ModLoaderStore.saved_extension_paths.has(mod_data.manifest.get_mod_id()):
ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()] = []
ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()].append(child_script_path)
Expand Down
2 changes: 1 addition & 1 deletion addons/mod_loader/api/profile.gd
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ static func _generate_mod_list_entry(mod_id: String, is_active: bool) -> Diction
# Set the current_config if the mod has a config schema and is active
if is_active and not ModLoaderConfig.get_config_schema(mod_id).is_empty():
var current_config: ModConfig = ModLoaderStore.mod_data[mod_id].current_config
if current_config and current_config.is_valid:
if current_config and current_config.is_valid():
# Set to the current_config name if valid
mod_list_entry.current_config = current_config.name
else:
Expand Down
1 change: 1 addition & 0 deletions addons/mod_loader/internal/dependency.gd
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ static func check_dependencies(mod: ModData, is_required := true, dependency_cha
_handle_missing_dependency(mod_id, dependency_id)
# Flag the mod so it's not loaded later
mod.is_loadable = false
mod.is_active = false
else:
var dependency: ModData = ModLoaderStore.mod_data[dependency_id]

Expand Down
10 changes: 4 additions & 6 deletions addons/mod_loader/internal/file.gd
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ static func get_json_as_dict_from_zip(zip_path: String, file_path: String, is_fu
for path in reader.get_files():
if Array(path.rsplit("/", false, 1)).back() == file_path:
full_path = path
if not full_path:
#if not full_path:
if full_path.is_empty():
ModLoaderLog.error("File was not found in zip at path %s" % [file_path], LOG_NAME)
return {}

Expand Down Expand Up @@ -194,11 +195,8 @@ static func file_exists_in_zip(zip_path: String, path: String) -> bool:
var reader := zip_reader_open(zip_path)
if not reader:
return false

if _ModLoaderGodot.is_version_below(_ModLoaderGodot.ENGINE_VERSION_HEX_4_2_0):
return reader.get_files().has(path.trim_prefix("res://"))
else:
return reader.file_exists(path.trim_prefix("res://"))

return reader.get_files().has(path.trim_prefix("res://"))


static func get_mod_dir_name_in_zip(zip_path: String) -> String:
Expand Down
11 changes: 0 additions & 11 deletions addons/mod_loader/internal/godot.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ const AUTOLOAD_CONFIG_HELP_MSG := "To configure your autoloads, go to Project >
const ENGINE_VERSION_HEX_4_2_2 := 0x040202
const ENGINE_VERSION_HEX_4_2_0 := 0x040200

static var engine_version_hex: int = Engine.get_version_info().hex


# Check autoload positions:
# Ensure 1st autoload is `ModLoaderStore`, and 2nd is `ModLoader`.
static func check_autoload_positions() -> void:
Expand Down Expand Up @@ -106,11 +103,3 @@ static func get_autoload_index(autoload_name: String) -> int:
var autoload_index := autoloads.find(autoload_name)

return autoload_index


static func is_version_below(version_hex: int) -> bool:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like is_version_below is still called in file.gd

return engine_version_hex < version_hex


static func is_version_above(version_hex: int) -> bool:
return engine_version_hex > version_hex
5 changes: 1 addition & 4 deletions addons/mod_loader/internal/hooks.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,10 @@ extends Object

const LOG_NAME := "ModLoader:Hooks"

static var any_mod_hooked := false


## Internal ModLoader method. [br]
## To add hooks from a mod use [method ModLoaderMod.add_hook].
static func add_hook(mod_callable: Callable, script_path: String, method_name: String) -> void:
any_mod_hooked = true
ModLoaderStore.any_mod_hooked = true
var hash := get_hook_hash(script_path, method_name)
if not ModLoaderStore.modding_hooks.has(hash):
ModLoaderStore.modding_hooks[hash] = []
Expand Down
Loading