Sets up initial project structure
Initializes the project with core files including: - Editor configuration (.editorconfig, .gitattributes, .gitignore, .vscode/settings.json) - Log.gd addon for enhanced debugging - Loggie addon for advanced logging - Project assets folder
This commit is contained in:
parent
f8140c83ff
commit
02b3be35b0
144 changed files with 7399 additions and 0 deletions
335
addons/loggie/version_management/loggie_update.gd
Normal file
335
addons/loggie/version_management/loggie_update.gd
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
@tool
|
||||
class_name LoggieUpdate extends Node
|
||||
|
||||
## Emitted when this update fails.
|
||||
signal failed()
|
||||
|
||||
## Emitted when this update succeeds.
|
||||
signal succeeded()
|
||||
|
||||
## Emitted when this declares that it has made some progress.
|
||||
signal progress(value : float)
|
||||
|
||||
## Emitted when this declares that it wants a new status/substatus message to be used.
|
||||
signal status_changed(status_msg : Variant, substatus_msg : Variant)
|
||||
|
||||
## Emitted when this update is starting.
|
||||
signal starting()
|
||||
|
||||
## Emitted when the 'is_in_progress' status of this update changes.
|
||||
signal is_in_progress_changed(new_value : bool)
|
||||
|
||||
## The path to the directory that should have a temporary file created and filled with the patch zipball buffer.
|
||||
const TEMP_FILES_DIR = "user://"
|
||||
|
||||
## If this is set to a non-empty string, it will be used as the directory into which the new update will be
|
||||
## installed. Used for testing/debugging. When set to empty string, Loggie will automatically figure out
|
||||
## where it is being updated from and use that directory instead.
|
||||
const ALT_LOGGIE_PLUGIN_CONTAINER_DIR = ""
|
||||
|
||||
## The domain from which status report [LoggieMsg]s from this update will be logged from.
|
||||
const REPORTS_DOMAIN : String = "loggie_update_status_reports"
|
||||
|
||||
## Stores a reference to the logger that's requesting this update.
|
||||
var _logger : Variant
|
||||
|
||||
## The URL used to visit a page that contains the release notes for this update.
|
||||
var release_notes_url = ""
|
||||
|
||||
## Stores a reference to the previous version the connected [member _logger] is/was using.
|
||||
var prev_version : LoggieVersion = null
|
||||
|
||||
## Stores a reference to the new version the connected [member _logger] should be using after the update.
|
||||
var new_version : LoggieVersion = null
|
||||
|
||||
## Indicates whether this update is currently in progress.
|
||||
var is_in_progress : bool = false
|
||||
|
||||
## Whether the update should retain or purge the backup it makes of the previous version files once it is done
|
||||
## installing and applying the new update.
|
||||
var _clean_up_backup_files : bool = true
|
||||
|
||||
func _init(_prev_version : LoggieVersion, _new_version : LoggieVersion) -> void:
|
||||
self.prev_version = _prev_version
|
||||
self.new_version = _new_version
|
||||
|
||||
## Returns a reference to the logger that's requesting this update.
|
||||
func get_logger() -> Variant:
|
||||
return self._logger
|
||||
|
||||
## Sets the URL used to visit a page that contains the release notes for this update.
|
||||
func set_release_notes_url(url : String) -> void:
|
||||
self.release_notes_url = url
|
||||
|
||||
## Sets whether this window is currently performing the update.
|
||||
func set_is_in_progress(value : bool) -> void:
|
||||
self.is_in_progress = value
|
||||
self.is_in_progress_changed.emit(value)
|
||||
|
||||
## Tries to start the version update. Prevents the update from starting
|
||||
## if something is not configured correctly and pushes a warning/error.
|
||||
func try_start():
|
||||
if Engine.has_meta("LoggieUpdateSuccessful") and Engine.get_meta("LoggieUpdateSuccessful"):
|
||||
# No plan to allow multiple updates to run during a single Engine session anyway so no need to start another one.
|
||||
# Also, this helps with internal testing of the updater and prevents an updated plugin from auto-starting another update
|
||||
# when dealing with proxy versions.
|
||||
return
|
||||
|
||||
if self._logger == null:
|
||||
push_warning("Attempt to start Loggie update failed - member '_logger' on the LoggieUpdate object is null.")
|
||||
return
|
||||
|
||||
if self.is_in_progress:
|
||||
push_warning("Attempt to start Loggie update failed - the update is already in progress.")
|
||||
return
|
||||
|
||||
if self.new_version == null or self.prev_version == null:
|
||||
push_warning("Attempt to start Loggie update failed - the updater prompt has the 'new_version' or 'prev_version' variable at null value.")
|
||||
return
|
||||
elif !self.new_version.is_higher_than(self.prev_version):
|
||||
push_warning("Attempt to start Loggie update failed - the 'new_version' is not higher than 'prev_version'.")
|
||||
return
|
||||
|
||||
if self.new_version.has_meta("github_data"):
|
||||
var github_data = self.new_version.get_meta("github_data")
|
||||
if !github_data.has("zipball_url"):
|
||||
push_error("Attempt to start Loggie update failed - the meta key 'github_data' on the 'new_version' is a dictionary that does not contain the required 'zipball_url' key/value pair.")
|
||||
return
|
||||
else:
|
||||
push_error("Attempt to start Loggie update failed - the meta key 'github_data' on the 'new_version' was not found.")
|
||||
return
|
||||
|
||||
_start()
|
||||
|
||||
## Internal function. Starts the updating of the [param _logger] to the [param new_version].
|
||||
## Do not run without verification that configuration is correct.
|
||||
## Use [method try_start] to call this safely.
|
||||
func _start():
|
||||
var loggie = self.get_logger()
|
||||
|
||||
loggie.msg("Loggie is updating from version {v_prev} to {v_new}.".format({
|
||||
"v_prev" : self.prev_version,
|
||||
"v_new" : self.new_version
|
||||
})).domain(REPORTS_DOMAIN).color(Color.ORANGE).box(12).info()
|
||||
|
||||
set_is_in_progress(true)
|
||||
starting.emit()
|
||||
|
||||
# Make request to configured endpoint.
|
||||
var update_data = self.new_version.get_meta("github_data")
|
||||
var http_request = HTTPRequest.new()
|
||||
loggie.add_child(http_request)
|
||||
http_request.request_completed.connect(_on_download_request_completed)
|
||||
http_request.request(update_data.zipball_url)
|
||||
|
||||
## Internal callback function.
|
||||
## Defines what happens when new update content is successfully downloaded from GitHub.
|
||||
## Called automatically during [method _start] if everything is going according to plan.
|
||||
func _on_download_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var loggie = self.get_logger()
|
||||
|
||||
if loggie == null:
|
||||
_failure("The _logger used by the updater window is null.")
|
||||
return
|
||||
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
_failure("Download request returned non-zero code: " + str(result))
|
||||
return
|
||||
|
||||
#region || Prepare: Define variables and callbacks that will be used throughout.
|
||||
# The path to the directory which is supposed to contain the plugin directory.
|
||||
# This will usually be 'res://addons/', but could be anything else too. We'll read it dynamically
|
||||
# from the connected logger to guarantee correctness.
|
||||
var LOGGIE_PLUGIN_CONTAINER_DIR = ALT_LOGGIE_PLUGIN_CONTAINER_DIR if !ALT_LOGGIE_PLUGIN_CONTAINER_DIR.is_empty() else loggie.get_directory_path().get_base_dir() + "/"
|
||||
|
||||
# The path to the `loggie` plugin directory.
|
||||
var LOGGIE_PLUGIN_DIR = ProjectSettings.globalize_path(LOGGIE_PLUGIN_CONTAINER_DIR.path_join("loggie/"))
|
||||
|
||||
# The full path filename of the temporary .zip archive that will be created to store the downloaded data.
|
||||
var TEMP_ZIP_FILE_PATH = ProjectSettings.globalize_path(TEMP_FILES_DIR.path_join("_temp_loggie_{ver}.zip".format({"ver": str(new_version)})))
|
||||
|
||||
# The path to the directory where a temporary backup of current loggie plugin files will be copied to.
|
||||
# (will be created if doesn't exist).
|
||||
var TEMP_PREV_VER_FILES_DIR_PATH = ProjectSettings.globalize_path(TEMP_FILES_DIR.path_join("_temp_loggie_{ver}_backup".format({"ver": str(prev_version)})))
|
||||
|
||||
# A callable that can be reused within this function that cleans up the temporary and unused directories,
|
||||
# once this function comes to a conclusion.
|
||||
var clean_up : Callable = func():
|
||||
if FileAccess.file_exists(TEMP_ZIP_FILE_PATH):
|
||||
OS.move_to_trash(TEMP_ZIP_FILE_PATH)
|
||||
if DirAccess.dir_exists_absolute(TEMP_PREV_VER_FILES_DIR_PATH) and self._clean_up_backup_files:
|
||||
OS.move_to_trash(TEMP_PREV_VER_FILES_DIR_PATH)
|
||||
|
||||
# A callable that can be used to replace the currently existing Loggie plugin directory
|
||||
# with whatever is currently (temporarily) stored as its backup.
|
||||
var revert_to_backup = func():
|
||||
if FileAccess.file_exists(LOGGIE_PLUGIN_DIR):
|
||||
OS.move_to_trash(LOGGIE_PLUGIN_DIR)
|
||||
if DirAccess.dir_exists_absolute(TEMP_PREV_VER_FILES_DIR_PATH):
|
||||
DirAccess.rename_absolute(TEMP_PREV_VER_FILES_DIR_PATH, LOGGIE_PLUGIN_DIR)
|
||||
|
||||
#endregion
|
||||
|
||||
#region || Step 1: Store the downloaded content into a temporary zip file.
|
||||
send_progress_update(20, "Processing Files", "Storing patch locally...")
|
||||
|
||||
var zip_file: FileAccess = FileAccess.open(TEMP_ZIP_FILE_PATH, FileAccess.WRITE)
|
||||
if zip_file == null:
|
||||
_failure("Failed to open temp. file for writing: {path}".format({"path": TEMP_ZIP_FILE_PATH}))
|
||||
clean_up.call()
|
||||
return
|
||||
|
||||
zip_file.store_buffer(body)
|
||||
zip_file.close()
|
||||
#endregion
|
||||
|
||||
#region || Step 2: Make a temporary backup of the currently used Loggie plugin directory.
|
||||
send_progress_update(30, "Processing Files", "Backing up previous version files...")
|
||||
|
||||
if !DirAccess.dir_exists_absolute(LOGGIE_PLUGIN_DIR):
|
||||
_failure("The Loggie plugin directory ({path}) could not be found.".format({
|
||||
"path" : LOGGIE_PLUGIN_DIR
|
||||
}))
|
||||
clean_up.call()
|
||||
return
|
||||
|
||||
var copy_prev_ver_result = LoggieTools.copy_dir_absolute(LOGGIE_PLUGIN_DIR, TEMP_PREV_VER_FILES_DIR_PATH, true)
|
||||
if copy_prev_ver_result.errors.size() > 0:
|
||||
var copy_prev_var_result_errors_msg = LoggieMsg.new("Errors encountered:")
|
||||
for error in copy_prev_ver_result.errors:
|
||||
copy_prev_var_result_errors_msg.nl().add(error_string(error))
|
||||
_failure(copy_prev_var_result_errors_msg.string())
|
||||
clean_up.call()
|
||||
return
|
||||
#endregion
|
||||
|
||||
#region || Step 3: Remove currently used Loggie plugin directory and create a new one in its place populated with new version files.
|
||||
send_progress_update(50, "Processing Files", "Copying new version files...")
|
||||
var zip_reader: ZIPReader = ZIPReader.new()
|
||||
var zip_reader_open_error = zip_reader.open(TEMP_ZIP_FILE_PATH)
|
||||
if zip_reader_open_error != OK:
|
||||
_failure("Attempt to open temp. file(s) archive at {path} failed with error: {err_str}".format({
|
||||
"path": LOGGIE_PLUGIN_DIR,
|
||||
"err_str" : error_string(zip_reader_open_error)
|
||||
}))
|
||||
clean_up.call()
|
||||
return
|
||||
|
||||
# Trash the previously existing loggie plugin dir entirely.
|
||||
# A new one will be created in a moment.
|
||||
OS.move_to_trash(LOGGIE_PLUGIN_DIR)
|
||||
|
||||
# Get a list of all files and dirs in the zip.
|
||||
var files : PackedStringArray = zip_reader.get_files()
|
||||
|
||||
# This will always be the "addons" directory in the zip archive in which we expect
|
||||
# to find the "loggie" directory containing the plugin.
|
||||
var base_path_in_zip = files[1]
|
||||
|
||||
# Remove the first 2 parts of the path that we won't be needing at all.
|
||||
files.remove_at(0)
|
||||
files.remove_at(0)
|
||||
|
||||
# Create all needed files and directories.
|
||||
for path in files:
|
||||
var new_file_path: String = path.replace(base_path_in_zip, "")
|
||||
if path.ends_with("/"):
|
||||
DirAccess.make_dir_recursive_absolute(LOGGIE_PLUGIN_CONTAINER_DIR + new_file_path)
|
||||
else:
|
||||
var abs_path = LOGGIE_PLUGIN_CONTAINER_DIR + new_file_path
|
||||
var file : FileAccess = FileAccess.open(abs_path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
_failure("Error while storing buffer data into temporary files - write target directory or file {target} gave the error: {error}".format({
|
||||
"error" : error_string(FileAccess.get_open_error()),
|
||||
"target" : abs_path
|
||||
}))
|
||||
revert_to_backup.call()
|
||||
clean_up.call()
|
||||
return
|
||||
else:
|
||||
var file_content = zip_reader.read_file(path)
|
||||
file.store_buffer(file_content)
|
||||
file.close()
|
||||
|
||||
zip_reader.close()
|
||||
#endregion
|
||||
|
||||
#region || Step 4: Move the user's 'custom_settings.gd' to the new version directory if it existed in prev version.
|
||||
send_progress_update(70, "Processing Files", "Reapplying custom settings...")
|
||||
var CUSTOM_SETTINGS_IN_PREV_VER_PATH = TEMP_PREV_VER_FILES_DIR_PATH.path_join("custom_settings.gd")
|
||||
if FileAccess.file_exists(CUSTOM_SETTINGS_IN_PREV_VER_PATH):
|
||||
var CUSTOM_SETTINGS_IN_NEW_VER_PATH = ProjectSettings.globalize_path(LOGGIE_PLUGIN_DIR.path_join("custom_settings.gd"))
|
||||
var custom_settings_copy_error = DirAccess.copy_absolute(CUSTOM_SETTINGS_IN_PREV_VER_PATH, CUSTOM_SETTINGS_IN_NEW_VER_PATH)
|
||||
if custom_settings_copy_error != OK:
|
||||
push_error("Attempt to copy the 'custom_settings.gd' file from {p1} to {p2} failed with error: {error}".format({
|
||||
"p1" : CUSTOM_SETTINGS_IN_PREV_VER_PATH,
|
||||
"p2" : CUSTOM_SETTINGS_IN_NEW_VER_PATH,
|
||||
"error" : error_string(custom_settings_copy_error)
|
||||
}))
|
||||
#endregion
|
||||
|
||||
#region || Step 5: Move the user's 'channels/custom_channels' directory to the new version if it existed in prev version.
|
||||
send_progress_update(80, "Processing Files", "Reapplying custom channels...")
|
||||
var CUSTOM_CHANNELS_IN_PREV_VER_PATH = ProjectSettings.globalize_path(TEMP_PREV_VER_FILES_DIR_PATH.path_join("channels/custom_channels/"))
|
||||
if DirAccess.dir_exists_absolute(CUSTOM_CHANNELS_IN_PREV_VER_PATH):
|
||||
var CUSTOM_CHANNELS_IN_NEW_VER_PATH = ProjectSettings.globalize_path(LOGGIE_PLUGIN_DIR.path_join("channels/custom_channels/"))
|
||||
var copy_prev_ver_custom_channels_result = LoggieTools.copy_dir_absolute(CUSTOM_CHANNELS_IN_PREV_VER_PATH, CUSTOM_CHANNELS_IN_NEW_VER_PATH, true)
|
||||
if copy_prev_ver_custom_channels_result.errors.size() > 0:
|
||||
var copy_prev_var_result_errors_msg = LoggieMsg.new("Errors encountered:")
|
||||
for error in copy_prev_ver_result.errors:
|
||||
copy_prev_var_result_errors_msg.nl().add(error_string(error))
|
||||
push_error("Attempt to copy the 'channels/custom_channels' directory from {p1} to {p2} failed with error: {error}".format({
|
||||
"p1" : CUSTOM_CHANNELS_IN_PREV_VER_PATH,
|
||||
"p2" : CUSTOM_CHANNELS_IN_NEW_VER_PATH,
|
||||
"error" : copy_prev_var_result_errors_msg.string()
|
||||
}))
|
||||
else:
|
||||
print("The {path} directory doesn't exist.".format({"path": CUSTOM_CHANNELS_IN_PREV_VER_PATH}))
|
||||
#endregion
|
||||
|
||||
#region || Step 6: Clean up temporarily created files and close filewrite.
|
||||
send_progress_update(90, "Processing Files", "Cleaning up...")
|
||||
clean_up.call()
|
||||
#endregion
|
||||
|
||||
#region || Step 7: Declare successful. Wrap up.
|
||||
send_progress_update(100, "Finishing up", "")
|
||||
_success()
|
||||
#endregion
|
||||
|
||||
## Internal function used at the end of the updating process if it is successfully completed.
|
||||
func _success():
|
||||
set_is_in_progress(false)
|
||||
|
||||
var msg = "💬 You may see temporary errors in the console due to Loggie files being re-scanned and reloaded on the spot.\nIt should be safe to dismiss them, but for the best experience, reload the Godot editor (and the plugin, if something seems wrong).\n\n🚩 If you see a 'Files have been modified on disk' window pop up, choose 'Discard local changes and reload' to accept incoming changes."
|
||||
status_changed.emit(null, msg)
|
||||
succeeded.emit()
|
||||
|
||||
print_rich(LoggieMsg.new("👀 Loggie updated to version {new_ver}!".format({"new_ver": self.new_version})).bold().color(Color.ORANGE).string())
|
||||
print_rich(LoggieMsg.new("\t📚 Release Notes: ").bold().msg("[url={url}]{url}[/url]".format({"url": release_notes_url})).color(Color.CORNFLOWER_BLUE).string())
|
||||
print_rich(LoggieMsg.new("\t💬 Support, Development & Feature Requests: ").bold().msg("[url=https://discord.gg/XPdxpMqmcs]https://discord.gg/XPdxpMqmcs[/url]").color(Color.CORNFLOWER_BLUE).string())
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
var editor_plugin = Engine.get_meta("LoggieEditorPlugin")
|
||||
editor_plugin.get_editor_interface().get_resource_filesystem().scan()
|
||||
editor_plugin.get_editor_interface().call_deferred("set_plugin_enabled", "loggie", true)
|
||||
editor_plugin.get_editor_interface().set_plugin_enabled("loggie", false)
|
||||
Engine.set_meta("LoggieUpdateSuccessful", true)
|
||||
print_rich("[b]Updater:[/b] ", msg)
|
||||
|
||||
## Internal function used to interrupt an ongoing update and cause it to fail.
|
||||
func _failure(status_msg : String):
|
||||
var loggie = self.get_logger()
|
||||
loggie.msg(status_msg).color(Color.SALMON).preprocessed(false).error()
|
||||
loggie.msg("\t💬 If this issue persists, consider reporting: ").bold().msg("https://github.com/Shiva-Shadowsong/loggie/issues").color(Color.CORNFLOWER_BLUE).preprocessed(false).info()
|
||||
set_is_in_progress(false)
|
||||
failed.emit()
|
||||
status_changed.emit(null, status_msg)
|
||||
|
||||
## Informs the listeners of the [signal progress] / [signal status_changed] signals about a change in the progress of the update.
|
||||
func send_progress_update(progress_amount : float, status_msg : String, substatus_msg : String):
|
||||
var loggie = self.get_logger()
|
||||
if !substatus_msg.is_empty():
|
||||
loggie.msg("•• ").msg(substatus_msg).domain(REPORTS_DOMAIN).preprocessed(false).info()
|
||||
progress.emit(progress_amount)
|
||||
status_changed.emit(status_msg, substatus_msg)
|
||||
Loading…
Add table
Add a link
Reference in a new issue