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