1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00
Pixelorama/src/HandleExtensions.gd
Emmanouil Papadeas f3ca6d1c33 Separate extension handling code from the Preferences UI
The preferences code only handles the UI related stuff, while HandleExtensions is now solely responsible for extension enabling, loading and uninstalling. This makes it possible to handle extensions without having the preferences dialog be in the middle.
2024-03-24 01:20:51 +02:00

247 lines
8.5 KiB
GDScript

class_name Extensions
extends Control
signal extension_loaded(extension: Extension, extension_name: String)
signal extension_uninstalled(file_name: String)
enum UninstallMode { KEEP_FILE, FILE_TO_BIN, REMOVE_PERMANENT }
const EXTENSIONS_PATH := "user://extensions"
const BUG_EXTENSIONS_PATH := "user://give_in_bug_report"
const BIN_ACTION := "trash"
var extensions := {} ## Extension name: Extension class
var extension_selected := -1
var damaged_extension: String
class Extension:
var file_name := ""
var display_name := ""
var description := ""
var author := ""
var version := ""
var license := ""
var nodes := []
var enabled: bool:
set(value):
enabled = value
enabled_once = true
var internal := false
var enabled_once := false
func serialize(dict: Dictionary) -> void:
if dict.has("name"):
file_name = dict["name"]
if dict.has("display_name"):
display_name = dict["display_name"]
if dict.has("description"):
description = dict["description"]
if dict.has("author"):
author = dict["author"]
if dict.has("version"):
version = dict["version"]
if dict.has("license"):
license = dict["license"]
if dict.has("nodes"):
nodes = dict["nodes"]
func _ready() -> void:
_add_internal_extensions()
var file_names: PackedStringArray = []
var dir := DirAccess.open("user://")
dir.make_dir(EXTENSIONS_PATH)
dir = DirAccess.open(EXTENSIONS_PATH)
if DirAccess.get_open_error() == OK:
dir.list_dir_begin()
var file_name := dir.get_next()
while file_name != "":
var ext := file_name.to_lower().get_extension()
if not dir.current_is_dir() and ext in ["pck", "zip"]:
file_names.append(file_name)
file_name = dir.get_next()
dir.list_dir_end()
if file_names.is_empty():
return
for file_name in file_names:
_add_extension(file_name)
## This is an empty function at the moment, but internal extensions here should be added here
## For example:
## [code]_load_extension("ExtensionName", true)[/code]
func _add_internal_extensions() -> void:
pass
func install_extension(path: String) -> void:
var file_name := path.get_file()
var err := DirAccess.copy_absolute(path, EXTENSIONS_PATH.path_join(file_name))
if err != OK:
print(err)
return
_add_extension(file_name)
func _add_extension(file_name: String) -> void:
var tester_file: FileAccess # For testing and deleting damaged extensions
# Remove any extension that was proven guilty before this extension is loaded
if FileAccess.file_exists(EXTENSIONS_PATH.path_join("Faulty.txt")):
# This code will only run if pixelorama crashed
var faulty_path := EXTENSIONS_PATH.path_join("Faulty.txt")
tester_file = FileAccess.open(faulty_path, FileAccess.READ)
damaged_extension = tester_file.get_as_text()
tester_file.close()
# don't delete the extension permanently
# (so that it may be given to the developer in the bug report)
DirAccess.make_dir_recursive_absolute(BUG_EXTENSIONS_PATH)
DirAccess.rename_absolute(
EXTENSIONS_PATH.path_join(damaged_extension),
BUG_EXTENSIONS_PATH.path_join(damaged_extension)
)
DirAccess.remove_absolute(EXTENSIONS_PATH.path_join("Faulty.txt"))
# Don't load a deleted extension
if damaged_extension == file_name:
# This code will only run if pixelorama crashed
damaged_extension = ""
return
# The new (about to load) extension will be considered guilty till it's proven innocent
tester_file = FileAccess.open(EXTENSIONS_PATH.path_join("Faulty.txt"), FileAccess.WRITE)
tester_file.store_string(file_name)
tester_file.close()
if extensions.has(file_name):
uninstall_extension(file_name, UninstallMode.KEEP_FILE)
# Wait two frames so the previous nodes can get freed
await get_tree().process_frame
await get_tree().process_frame
var file_path := EXTENSIONS_PATH.path_join(file_name)
var success := ProjectSettings.load_resource_pack(file_path)
if !success:
# Don't delete the extension
# Context: pixelorama deletes v0.11.x extensions when you open v1.0, this will prevent it.
print("EXTENSION ERROR: Failed loading resource pack %s." % file_name)
print("There may be errors in extension code or extension is incompatible")
# Delete the faulty.txt, its fate has already been decided
DirAccess.remove_absolute(EXTENSIONS_PATH.path_join("Faulty.txt"))
return
_load_extension(file_name)
func _load_extension(extension_file_or_folder_name: StringName, internal := false) -> void:
var file_name_no_ext := extension_file_or_folder_name.get_basename()
var extension_path := "res://src/Extensions/%s/" % file_name_no_ext
var extension_config_file_path := extension_path.path_join("extension.json")
var extension_config_file := FileAccess.open(extension_config_file_path, FileAccess.READ)
var err := FileAccess.get_open_error()
if err != OK:
print("Error loading config file: ", err, " (", error_string(err), ")")
extension_config_file.close()
return
var test_json_conv := JSON.new()
test_json_conv.parse(extension_config_file.get_as_text())
var extension_json = test_json_conv.get_data()
extension_config_file.close()
if not extension_json:
print("No JSON data found.")
return
if extension_json.has("supported_api_versions"):
var supported_api_versions = extension_json["supported_api_versions"]
if typeof(supported_api_versions) == TYPE_ARRAY:
supported_api_versions = PackedInt32Array(supported_api_versions)
if not ExtensionsApi.get_api_version() in supported_api_versions:
var err_text := (
"The extension %s will not work on this version of Pixelorama \n"
% file_name_no_ext
)
var required_text := str(
"Extension works on API versions: %s" % str(supported_api_versions),
"\n",
"But Pixelorama's API version is: %s" % ExtensionsApi.get_api_version()
)
Global.popup_error(str(err_text, required_text))
print("Incompatible API")
if !internal: # The file isn't created for internal extensions, no need for removal
# Don't put it in faulty, it's merely incompatible
DirAccess.remove_absolute(EXTENSIONS_PATH.path_join("Faulty.txt"))
return
var extension := Extension.new()
extension.serialize(extension_json)
extension.internal = internal
extensions[extension_file_or_folder_name] = extension
extension_loaded.emit(extension, extension_file_or_folder_name)
# Enable internal extensions if it is the first time they are being loaded
extension.enabled = Global.config_cache.get_value("extensions", extension.file_name, internal)
if extension.enabled:
enable_extension(extension)
# If an extension doesn't crash pixelorama then it is proven innocent
# And we should now delete its "Faulty.txt" file
if !internal: # the file isn't created for internal extensions, so no need to remove it
DirAccess.remove_absolute(EXTENSIONS_PATH.path_join("Faulty.txt"))
func enable_extension(extension: Extension, save_to_config := true) -> void:
var extension_path: String = "res://src/Extensions/%s/" % extension.file_name
# A unique id for the extension (currently set to file_name). More parameters (version etc.)
# can be easily added using the str() function. for example
# var id: String = str(extension.file_name, extension.version)
var id: String = extension.file_name
if extension.enabled:
ExtensionsApi.clear_history(extension.file_name)
for node in extension.nodes:
var scene_path: String = extension_path.path_join(node)
var extension_scene: PackedScene = load(scene_path)
if extension_scene:
var extension_node: Node = extension_scene.instantiate()
add_child(extension_node)
extension_node.add_to_group(id) # Keep track of what to remove later
else:
print("Failed to load extension %s" % id)
else:
for ext_node in get_children():
if ext_node.is_in_group(id): # Node for extension found
remove_child(ext_node)
ext_node.queue_free()
ExtensionsApi.check_sanity(extension.file_name)
if save_to_config:
Global.config_cache.set_value("extensions", extension.file_name, extension.enabled)
Global.config_cache.save("user://cache.ini")
func uninstall_extension(file_name := "", remove_mode := UninstallMode.REMOVE_PERMANENT) -> void:
var err := OK
match remove_mode:
UninstallMode.FILE_TO_BIN:
err = OS.move_to_trash(
ProjectSettings.globalize_path(EXTENSIONS_PATH).path_join(file_name)
)
UninstallMode.REMOVE_PERMANENT:
err = DirAccess.remove_absolute(EXTENSIONS_PATH.path_join(file_name))
if remove_mode != UninstallMode.KEEP_FILE:
if err != OK:
print(err)
return
var extension: Extension = extensions[file_name]
extension.enabled = false
enable_extension(extension, false)
extensions.erase(file_name)
extension_selected = -1
extension_uninstalled.emit(file_name)