155 lines
4.4 KiB
GDScript3
155 lines
4.4 KiB
GDScript3
|
|
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
|