Compare commits
1 Commits
main
...
plugin-loa
Author | SHA1 | Date | |
---|---|---|---|
ac9e94c9b0 |
249
plugin_loader.gd
Normal file
249
plugin_loader.gd
Normal file
@ -0,0 +1,249 @@
|
||||
## This resource works in two stages:
|
||||
## 1. loads resource packs from a specified location
|
||||
## 2. verifies each resource pack is conform to a spec
|
||||
## 3. list all plugins
|
||||
## 4. loads all plugins marked as "enabled"
|
||||
##
|
||||
## Notes: Enabling a plugin does _not_ run it. It's expected that the app would
|
||||
## be restarted for the loading to happen. If you want to immediately load and
|
||||
## unload, use the signals
|
||||
##
|
||||
## ## Expected Flow:
|
||||
##
|
||||
## 1. use `PluginLoader.restore().load_plugins()` to load all plugins
|
||||
## 2. use `PluginLoader.restore().provide_interface()` to get a bunch of
|
||||
## checkboxes to use in an UI
|
||||
## 3. that's it! You can also provide an initial set of plugins (expected to be
|
||||
## found with the app's files
|
||||
class_name PluginLoader extends Resource
|
||||
|
||||
################################################################################
|
||||
#
|
||||
# GENERAL SINGLETON BEHAVIOR
|
||||
#
|
||||
|
||||
## Default resource path.
|
||||
const _RESOURCE_PATH := "user://plugins.tres"
|
||||
|
||||
|
||||
## Loads the plugin loader resource.
|
||||
## Always returns a PluginLoader instance; if the path is wrong, a new instance will
|
||||
## be created.
|
||||
static func restore(path := _RESOURCE_PATH) -> PluginLoader:
|
||||
var file: PluginLoader = ResourceLoader.load(path, "PluginLoader")
|
||||
if file == null:
|
||||
return PluginLoader.new()
|
||||
file.refresh_from_disk()
|
||||
return file
|
||||
|
||||
|
||||
## Saves the current plugins status.
|
||||
## This is called automatically when changing a plugin status. You shouldn't have to
|
||||
## call it manually.
|
||||
func save() -> void:
|
||||
ResourceSaver.save(self, _RESOURCE_PATH)
|
||||
|
||||
|
||||
################################################################################
|
||||
#
|
||||
# PLUGIN PACKS LOADING
|
||||
#
|
||||
|
||||
enum STATUS{
|
||||
AVAILABLE,
|
||||
LOADED,
|
||||
ERROR,
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
## Defines where plugin packs should be fetched from
|
||||
@export var plugins_packs_directory := "user://"
|
||||
## An object of [plugin_name: String, status: STATUS]
|
||||
@export var plugins_status := {}
|
||||
## These represent plugins that come with the application
|
||||
@export var native_plugins := {}
|
||||
|
||||
## Lists all plugin packs from their designated location. Refreshes the "plugins
|
||||
## status" array
|
||||
## Returns a Dictionary with keys "added" and "removed", each respectively listing
|
||||
## all added or removed packs as strings
|
||||
func read_available_plugins() -> Dictionary:
|
||||
var dir := DirAccess.open(plugins_packs_directory)
|
||||
if dir == null:
|
||||
return {}
|
||||
dir.list_dir_begin()
|
||||
|
||||
var file_name = dir.get_next()
|
||||
|
||||
var old_plugins := plugins_status.duplicate()
|
||||
plugins_status = {}
|
||||
var removed: Array[String] = []
|
||||
var added: Array[String] = []
|
||||
|
||||
# read the resource packs directory
|
||||
while file_name != "":
|
||||
if not dir.current_is_dir():
|
||||
if file_name.get_extension().to_lower() == "pck":
|
||||
var plugin_name := file_name.get_basename()
|
||||
var path := plugins_packs_directory.path_join(file_name)
|
||||
var success := ProjectSettings.load_resource_pack(path, false)
|
||||
if not success:
|
||||
printerr("Could not load resource pack %s"%[file_name])
|
||||
else:
|
||||
if old_plugins.has(plugin_name):
|
||||
plugins_status[plugin_name] = old_plugins[plugin_name]
|
||||
else:
|
||||
added.append(plugin_name)
|
||||
plugins_status[plugin_name] = STATUS.AVAILABLE
|
||||
file_name = dir.get_next()
|
||||
|
||||
# read the native plugins list
|
||||
for plugin_name in native_plugins:
|
||||
if not old_plugins.has(plugin_name):
|
||||
plugins_status[plugin_name] = old_plugins[plugin_name]
|
||||
else:
|
||||
plugins_status[plugin_name] = native_plugins[plugin_name]
|
||||
|
||||
# check if anything was removed
|
||||
for plugin_name in old_plugins:
|
||||
if not plugins_status.has(plugin_name):
|
||||
removed.append(plugin_name)
|
||||
|
||||
save()
|
||||
|
||||
return {
|
||||
added = added,
|
||||
removed = removed,
|
||||
}
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
#
|
||||
# PLUGINS STATES
|
||||
#
|
||||
# Changing a plugin state does not mean anything before calling `load_plugins()`
|
||||
#
|
||||
|
||||
|
||||
signal plugin_status_changed(plugin_name: String, status: STATUS)
|
||||
|
||||
|
||||
## Sets a plugin on or off. Does not immediately load it.
|
||||
func set_plugin_status(plugin_name: String, status: STATUS) -> void:
|
||||
if not plugins_status.has(plugin_name):
|
||||
printerr("Plugin %s does not exist"%[plugin_name])
|
||||
return
|
||||
if plugins_status[plugin_name] == status:
|
||||
return
|
||||
plugins_status[plugin_name] = status
|
||||
plugin_status_changed.emit(plugin_name, status)
|
||||
save()
|
||||
|
||||
|
||||
|
||||
## Returns a plugin status.
|
||||
func get_plugin_status(plugin_name: String) -> STATUS:
|
||||
if not plugins_status.has(plugin_name):
|
||||
printerr("Plugin %s does not exist"%[plugin_name])
|
||||
return STATUS.NOT_FOUND
|
||||
return (plugins_status[plugin_name]) as STATUS
|
||||
|
||||
|
||||
################################################################################
|
||||
#
|
||||
# PLUGINS INSTANCING
|
||||
#
|
||||
|
||||
## Defines where plugins are expected to be found after a plugin pack is loaded
|
||||
## this is also where native plugins are located
|
||||
@export var plugins_local_directory := "res://mods"
|
||||
signal plugin_error(plugin_name: String)
|
||||
signal plugin_started(plugin_name: String)
|
||||
|
||||
|
||||
func start_plugin_if_enabled(plugin_name: String) -> bool:
|
||||
var status := get_plugin_status(plugin_name)
|
||||
match status:
|
||||
STATUS.LOADED:
|
||||
var instance := instance_plugin_if_valid(plugin_name)
|
||||
if instance == null:
|
||||
plugins_status[plugin_name] = STATUS.ERROR
|
||||
plugin_error.emit(plugin_name)
|
||||
else:
|
||||
instance.plugin_start()
|
||||
plugin_started.emit(plugin_name)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## This loads a local plugin and instances it. If the plugin comes from a resource pack,
|
||||
## the resource pack needs to have been loaded before
|
||||
func instance_plugin_if_valid(plugin_name: String) -> Plugin_V1:
|
||||
var entry_point_path := plugins_local_directory.path_join(plugin_name).path_join("plugin.gd")
|
||||
var entry_point_file := FileAccess.open(entry_point_path, FileAccess.READ)
|
||||
if entry_point_file == null:
|
||||
printerr("The entry point does not exist or is not readable")
|
||||
return null
|
||||
var entry_point_text := entry_point_file.get_as_text()
|
||||
if not entry_point_text.match("extends"):
|
||||
printerr("The entry point is not text, which is not allowed for security reasons")
|
||||
return null
|
||||
if entry_point_text.match("func _init():"):
|
||||
printerr("The entry point contains _init(), which is a security issue, mod will not load")
|
||||
return null
|
||||
var entry_point_script = load(entry_point_path)
|
||||
if entry_point_script == null:
|
||||
printerr("Tried loading %s but did not find the expected entry point"%[entry_point_path])
|
||||
return null
|
||||
var instance: Plugin_V1 = (entry_point_script as Plugin_V1).new()
|
||||
if not (instance is Plugin_V1):
|
||||
printerr("Tried loading %s but it is not the right format"%[entry_point_path])
|
||||
return null
|
||||
return instance
|
||||
|
||||
|
||||
################################################################################
|
||||
#
|
||||
# UTILITIES
|
||||
#
|
||||
|
||||
|
||||
## Utility function to provide a list of checkboxes that turn plugins on or off.
|
||||
func provide_interface() -> Array[CheckBox]:
|
||||
var checkboxes: Array[CheckBox] = []
|
||||
for plugin in plugins_status:
|
||||
var plugin_name: String = plugin as String
|
||||
var checkbox := CheckBox.new()
|
||||
checkbox.text = plugin_name.get_basename()
|
||||
checkbox.button_pressed = get_plugin_status(plugin_name) == STATUS.LOADED
|
||||
checkbox.toggled.connect(
|
||||
func checkbox_toggled(is_on: bool) -> void:
|
||||
set_plugin_status(plugin_name, STATUS.LOADED if is_on else STATUS.AVAILABLE)
|
||||
)
|
||||
checkboxes.append(checkbox)
|
||||
return checkboxes
|
||||
|
||||
|
||||
################################################################################
|
||||
#
|
||||
# BOOSTRAPPING
|
||||
#
|
||||
|
||||
## For each plugin that's on, load it.
|
||||
## This function makes no assumptions and no checks. Not loading plugins twice
|
||||
## is on you
|
||||
func load_plugins() -> void:
|
||||
var report := read_available_plugins()
|
||||
var removed := report.removed as Array[String]
|
||||
var added := report.added as Array[String]
|
||||
if removed.size():
|
||||
print_rich("Removed plugins %s"%[", ".join(removed)])
|
||||
if added.size():
|
||||
print_rich("Added plugins %s"%[", ".join(removed)])
|
||||
for plugin_name in plugins_status:
|
||||
start_plugin_if_enabled(plugin_name)
|
||||
|
||||
|
||||
|
||||
|
9
plugin_v1.gd
Normal file
9
plugin_v1.gd
Normal file
@ -0,0 +1,9 @@
|
||||
class_name Plugin_V1
|
||||
|
||||
|
||||
func plugin_start() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func plugin_stop() -> void:
|
||||
pass
|
Loading…
Reference in New Issue
Block a user