extends Node class_name PluginManager ## 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) ## All loaded plugin data: plugin_id -> Dictionary var _plugins: Dictionary = {} ## All tile definitions: tile_id -> Dictionary var _tiles: Dictionary = {} ## Plugin-id -> Array[tile_id] for reverse lookup var _plugin_tiles: Dictionary = {} func _ready() -> void: _scan_plugins() ## 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 # 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) 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 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 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