V-Panel/autoload/plugin_manager.gd

260 lines
8.1 KiB
GDScript3
Raw Permalink Normal View History

extends Node
## Scans res://plugins/ for plugin manifests (plugin.cfg) and provides
## access to tile definitions. Instantiate tiles by tile_id.
signal plugin_loaded(plugin_id: String, plugin_name: String)
signal plugin_load_failed(plugin_id: String, reason: String)
signal plugin_active_changed(plugin_id: String, active: bool)
signal plugin_setting_changed(plugin_id: String, key: String, value: Variant)
## All loaded plugin data: plugin_id -> Dictionary
var _plugins: Dictionary = {}
## Tracks which plugins are active (plugin_id -> bool). Default true.
var _plugin_active: Dictionary = {}
## All tile definitions: tile_id -> Dictionary
var _tiles: Dictionary = {}
## Plugin-id -> Array[tile_id] for reverse lookup
var _plugin_tiles: Dictionary = {}
## Per-plugin settings: plugin_id -> { key -> value }
var _plugin_settings: Dictionary = {}
const PLUGIN_SETTINGS_PATH: String = "res://config/plugin_settings.cfg"
func _ready() -> void:
_scan_plugins()
_load_plugin_settings()
## Scan res://plugins/*/plugin.cfg and load all manifests.
func _scan_plugins() -> void:
var dir := DirAccess.open("res://plugins")
if dir == null:
return
dir.list_dir_begin()
var entry: String = dir.get_next()
while entry != "":
if entry.begins_with("."):
entry = dir.get_next()
continue
if dir.current_is_dir():
var manifest_path := "res://plugins/%s/plugin.cfg" % entry
if FileAccess.file_exists(manifest_path):
_load_manifest(entry, manifest_path)
else:
plugin_load_failed.emit(entry, "Missing plugin.cfg")
entry = dir.get_next()
dir.list_dir_end()
func _load_manifest(plugin_id: String, path: String) -> void:
var cfg := ConfigFile.new()
if cfg.load(path) != OK:
plugin_load_failed.emit(plugin_id, "Failed to parse plugin.cfg")
return
var plugin_info := {
id = plugin_id,
name = cfg.get_value("plugin", "name", plugin_id),
version = cfg.get_value("plugin", "version", "0.1.0"),
description = cfg.get_value("plugin", "description", ""),
author = cfg.get_value("plugin", "author", ""),
base_path = path.get_base_dir(),
}
_plugins[plugin_id] = plugin_info
_plugin_active[plugin_id] = true
# Load tile definitions
var tile_count: int = cfg.get_value("tiles", "count", 0)
for i in range(tile_count):
var section := "tile_%d" % i
var tile_id: String = cfg.get_value(section, "id", "")
if tile_id.is_empty():
continue
# Scoped tile-id: "plugin_id/tile_id" ensures global uniqueness
var scoped_id := "%s/%s" % [plugin_id, tile_id]
if _tiles.has(scoped_id):
plugin_load_failed.emit(plugin_id, "Duplicate tile id: %s" % scoped_id)
continue
var tile_def := {
id = scoped_id,
plugin_id = plugin_id,
name = cfg.get_value(section, "name", tile_id),
scene = cfg.get_value(section, "scene", ""),
min_w = cfg.get_value(section, "min_w", 1),
min_h = cfg.get_value(section, "min_h", 1),
max_w = cfg.get_value(section, "max_w", 4),
max_h = cfg.get_value(section, "max_h", 4),
default_w = cfg.get_value(section, "default_w", 1),
default_h = cfg.get_value(section, "default_h", 1),
}
_tiles[scoped_id] = tile_def
if not _plugin_tiles.has(plugin_id):
_plugin_tiles[plugin_id] = []
_plugin_tiles[plugin_id].append(scoped_id)
# Load setting definitions
var setting_defs: Array[Dictionary] = []
var setting_count: int = cfg.get_value("settings", "count", 0)
for i in range(setting_count):
var s_section := "setting_%d" % i
var skey: String = cfg.get_value(s_section, "key", "")
if skey.is_empty():
continue
var sd: Dictionary = {
key = skey,
label = cfg.get_value(s_section, "label", skey),
type = cfg.get_value(s_section, "type", "string"),
}
# Read optional fields that don't have a default value
if cfg.has_section_key(s_section, "default"):
sd.default = cfg.get_value(s_section, "default")
if cfg.has_section_key(s_section, "min"):
sd.min = cfg.get_value(s_section, "min")
if cfg.has_section_key(s_section, "max"):
sd.max = cfg.get_value(s_section, "max")
if cfg.has_section_key(s_section, "step"):
sd.step = cfg.get_value(s_section, "step")
setting_defs.append(sd)
plugin_info.settings = setting_defs
plugin_loaded.emit(plugin_id, plugin_info.name)
## Returns a list of all loaded plugin IDs.
func get_plugin_ids() -> PackedStringArray:
var result: PackedStringArray = []
for pid in _plugins:
result.append(pid)
return result
## Returns plugin info Dictionary for the given plugin_id, or null.
func get_plugin(plugin_id: String) -> Dictionary:
return _plugins.get(plugin_id, {})
## Returns true if the plugin is active (tiles can be instantiated).
func is_plugin_active(plugin_id: String) -> bool:
return _plugin_active.get(plugin_id, true)
## Enable or disable a plugin. When disabled, tiles cannot be instantiated.
func set_plugin_active(plugin_id: String, active: bool) -> void:
if _plugin_active.get(plugin_id, true) == active:
return
_plugin_active[plugin_id] = active
plugin_active_changed.emit(plugin_id, active)
## Returns setting definitions for a plugin (from its manifest).
func get_plugin_settings(plugin_id: String) -> Array[Dictionary]:
var info: Dictionary = _plugins.get(plugin_id, {})
var defs: Array = info.get("settings", [])
return defs.duplicate()
## Read a plugin setting value, falling back to the default from the manifest.
func get_plugin_setting(plugin_id: String, key: String, default_value: Variant = null) -> Variant:
if _plugin_settings.has(plugin_id) and _plugin_settings[plugin_id].has(key):
return _plugin_settings[plugin_id][key]
# Fall back to manifest default
var info: Dictionary = _plugins.get(plugin_id, {})
for sd in info.get("settings", []):
if sd.get("key", "") == key:
return sd.get("default", default_value)
return default_value
## Persist a plugin setting and emit change signal.
func set_plugin_setting(plugin_id: String, key: String, value: Variant) -> void:
if not _plugin_settings.has(plugin_id):
_plugin_settings[plugin_id] = {}
if _plugin_settings[plugin_id].get(key) == value:
return
_plugin_settings[plugin_id][key] = value
_save_plugin_settings()
plugin_setting_changed.emit(plugin_id, key, value)
func _load_plugin_settings() -> void:
var cfg := ConfigFile.new()
if cfg.load(PLUGIN_SETTINGS_PATH) != OK:
return
for section in cfg.get_sections():
_plugin_settings[section] = {}
for key in cfg.get_section_keys(section):
_plugin_settings[section][key] = cfg.get_value(section, key)
func _save_plugin_settings() -> void:
var cfg := ConfigFile.new()
for pid in _plugin_settings:
for key in _plugin_settings[pid]:
cfg.set_value(pid, key, _plugin_settings[pid][key])
# Only write if there's data
if cfg.get_sections().is_empty():
return
DirAccess.make_dir_recursive_absolute("res://config")
cfg.save(PLUGIN_SETTINGS_PATH)
## Returns all tile definitions across all plugins.
func get_all_tile_defs() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for tid in _tiles:
result.append(_tiles[tid])
return result
## Returns a single tile definition by scoped tile_id, or null.
func get_tile_def(tile_id: String) -> Dictionary:
return _tiles.get(tile_id, {})
## Returns tile IDs belonging to a specific plugin.
func get_plugin_tile_ids(plugin_id: String) -> PackedStringArray:
var result: PackedStringArray = []
for tid in _plugin_tiles.get(plugin_id, []):
result.append(tid)
return result
## Instantiate a tile by its scoped tile_id.
## Returns the root Control node, or null on failure.
func instantiate_tile(tile_id: String) -> Control:
var def: Dictionary = _tiles.get(tile_id, {})
if def.is_empty():
return null
# Don't instantiate tiles from inactive plugins
var pid: String = def.get("plugin_id", "")
if not pid.is_empty() and not is_plugin_active(pid):
return null
var scene_path: String = def.get("scene", "")
if scene_path.is_empty():
return null
var scene := load(scene_path) as PackedScene
if scene == null:
return null
var instance: Control = scene.instantiate() as Control
if instance == null:
return null
# Tag the instance so the layout manager can correlate it
if instance.has_method("set_tile_id"):
instance.set_tile_id(tile_id)
if instance.has_method("set_tile_config"):
instance.set_tile_config(def)
return instance