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
 | 
			
		||||
		Reference in New Issue
	
	Block a user