Compare commits
	
		
			1 Commits
		
	
	
		
			godot-4-de
			...
			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