mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-18 17:19:50 +00:00
f3ca6d1c33
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.
247 lines
8.5 KiB
GDScript
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)
|