- Added the Collection Element and moved parts of the List logic to collection - Added a State Management System - Added a better system to change the position/size of elements - Removed the state plugin
451 lines
15 KiB
Lua
451 lines
15 KiB
Lua
local args = table.pack(...)
|
|
local dir = fs.getDir(args[2] or "basalt")
|
|
local subDir = args[1]
|
|
if(dir==nil)then
|
|
error("Unable to find directory "..args[2].." please report this bug to our discord.")
|
|
end
|
|
|
|
local log = require("log")
|
|
local defaultPath = package.path
|
|
local format = "path;/path/?.lua;/path/?/init.lua;"
|
|
local main = format:gsub("path", dir)
|
|
|
|
--- This class manages elements and plugins. It loads elements and plugins from the elements and plugins directories
|
|
--- and then applies the plugins to the elements. It also provides a way to get elements and APIs.
|
|
--- @class ElementManager
|
|
local ElementManager = {}
|
|
ElementManager._elements = {}
|
|
ElementManager._plugins = {}
|
|
ElementManager._APIs = {}
|
|
ElementManager._config = {
|
|
autoLoadMissing = false,
|
|
allowRemoteLoading = false,
|
|
allowDiskLoading = true,
|
|
remoteSources = {},
|
|
diskMounts = {},
|
|
useGlobalCache = false,
|
|
globalCacheName = "_BASALT_ELEMENT_CACHE"
|
|
}
|
|
|
|
local elementsDirectory = fs.combine(dir, "elements")
|
|
local pluginsDirectory = fs.combine(dir, "plugins")
|
|
|
|
log.info("Loading elements from "..elementsDirectory)
|
|
if fs.exists(elementsDirectory) then
|
|
for _, file in ipairs(fs.list(elementsDirectory)) do
|
|
local name = file:match("(.+).lua")
|
|
if name then
|
|
log.debug("Found element: "..name)
|
|
ElementManager._elements[name] = {
|
|
class = nil,
|
|
plugins = {},
|
|
loaded = false,
|
|
source = "local",
|
|
path = nil
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
log.info("Loading plugins from "..pluginsDirectory)
|
|
if fs.exists(pluginsDirectory) then
|
|
for _, file in ipairs(fs.list(pluginsDirectory)) do
|
|
local name = file:match("(.+).lua")
|
|
if name then
|
|
log.debug("Found plugin: "..name)
|
|
local plugin = require(fs.combine("plugins", name))
|
|
if type(plugin) == "table" then
|
|
for k,v in pairs(plugin) do
|
|
if(k ~= "API")then
|
|
if(ElementManager._plugins[k]==nil)then
|
|
ElementManager._plugins[k] = {}
|
|
end
|
|
table.insert(ElementManager._plugins[k], v)
|
|
else
|
|
ElementManager._APIs[name] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if(minified)then
|
|
if(minified_elementDirectory==nil)then
|
|
error("Unable to find minified_elementDirectory please report this bug to our discord.")
|
|
end
|
|
for name,v in pairs(minified_elementDirectory)do
|
|
ElementManager._elements[name:gsub(".lua", "")] = {
|
|
class = nil,
|
|
plugins = {},
|
|
loaded = false,
|
|
source = "local",
|
|
path = nil
|
|
}
|
|
end
|
|
if(minified_pluginDirectory==nil)then
|
|
error("Unable to find minified_pluginDirectory please report this bug to our discord.")
|
|
end
|
|
for name,_ in pairs(minified_pluginDirectory)do
|
|
local plugName = name:gsub(".lua", "")
|
|
local plugin = require(fs.combine("plugins", plugName))
|
|
if type(plugin) == "table" then
|
|
for k,v in pairs(plugin) do
|
|
if(k ~= "API")then
|
|
if(ElementManager._plugins[k]==nil)then
|
|
ElementManager._plugins[k] = {}
|
|
end
|
|
table.insert(ElementManager._plugins[k], v)
|
|
else
|
|
ElementManager._APIs[plugName] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function saveToGlobalCache(name, element)
|
|
if not ElementManager._config.useGlobalCache then return end
|
|
|
|
if not _G[ElementManager._config.globalCacheName] then
|
|
_G[ElementManager._config.globalCacheName] = {}
|
|
end
|
|
|
|
_G[ElementManager._config.globalCacheName][name] = element
|
|
log.debug("Cached element in _G: "..name)
|
|
end
|
|
|
|
local function loadFromGlobalCache(name)
|
|
if not ElementManager._config.useGlobalCache then return nil end
|
|
|
|
if _G[ElementManager._config.globalCacheName] and
|
|
_G[ElementManager._config.globalCacheName][name] then
|
|
log.debug("Loaded element from _G cache: "..name)
|
|
return _G[ElementManager._config.globalCacheName][name]
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
--- Configures the ElementManager
|
|
--- @param config table Configuration options
|
|
function ElementManager.configure(config)
|
|
for k, v in pairs(config) do
|
|
if ElementManager._config[k] ~= nil then
|
|
ElementManager._config[k] = v
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Registers a disk mount point for loading elements
|
|
--- @param mountPath string The path to the disk mount
|
|
function ElementManager.registerDiskMount(mountPath)
|
|
if not fs.exists(mountPath) then
|
|
error("Disk mount path does not exist: "..mountPath)
|
|
end
|
|
table.insert(ElementManager._config.diskMounts, mountPath)
|
|
log.info("Registered disk mount: "..mountPath)
|
|
|
|
local elementsPath = fs.combine(mountPath, "elements")
|
|
if fs.exists(elementsPath) then
|
|
for _, file in ipairs(fs.list(elementsPath)) do
|
|
local name = file:match("(.+).lua")
|
|
if name then
|
|
if not ElementManager._elements[name] then
|
|
log.debug("Found element on disk: "..name)
|
|
ElementManager._elements[name] = {
|
|
class = nil,
|
|
plugins = {},
|
|
loaded = false,
|
|
source = "disk",
|
|
path = fs.combine(elementsPath, file)
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Registers a remote source for an element
|
|
--- @param elementName string The name of the element
|
|
--- @param url string The URL to load the element from
|
|
function ElementManager.registerRemoteSource(elementName, url)
|
|
if not ElementManager._config.allowRemoteLoading then
|
|
error("Remote loading is disabled. Enable with ElementManager.configure({allowRemoteLoading = true})")
|
|
end
|
|
ElementManager._config.remoteSources[elementName] = url
|
|
|
|
if not ElementManager._elements[elementName] then
|
|
ElementManager._elements[elementName] = {
|
|
class = nil,
|
|
plugins = {},
|
|
loaded = false,
|
|
source = "remote",
|
|
path = url
|
|
}
|
|
else
|
|
ElementManager._elements[elementName].source = "remote"
|
|
ElementManager._elements[elementName].path = url
|
|
end
|
|
|
|
log.info("Registered remote source for "..elementName..": "..url)
|
|
end
|
|
|
|
local function loadFromRemote(url)
|
|
if not http then
|
|
error("HTTP API is not available. Enable it in your CC:Tweaked config.")
|
|
end
|
|
|
|
log.info("Loading element from remote: "..url)
|
|
|
|
local response = http.get(url)
|
|
if not response then
|
|
error("Failed to download from: "..url)
|
|
end
|
|
|
|
local content = response.readAll()
|
|
response.close()
|
|
|
|
if not content or content == "" then
|
|
error("Empty response from: "..url)
|
|
end
|
|
|
|
local func, err = load(content, url, "t", _ENV)
|
|
if not func then
|
|
error("Failed to load element from "..url..": "..tostring(err))
|
|
end
|
|
|
|
local element = func()
|
|
return element
|
|
end
|
|
|
|
local function loadFromDisk(path)
|
|
if not fs.exists(path) then
|
|
error("Element file does not exist: "..path)
|
|
end
|
|
|
|
log.info("Loading element from disk: "..path)
|
|
|
|
local func, err = loadfile(path)
|
|
if not func then
|
|
error("Failed to load element from "..path..": "..tostring(err))
|
|
end
|
|
|
|
local element = func()
|
|
return element
|
|
end
|
|
|
|
--- Tries to load an element from any available source
|
|
--- @param name string The element name
|
|
--- @return boolean success Whether the element was loaded
|
|
function ElementManager.tryAutoLoad(name)
|
|
-- Try disk mounts first
|
|
if ElementManager._config.allowDiskLoading then
|
|
for _, mountPath in ipairs(ElementManager._config.diskMounts) do
|
|
local elementsPath = fs.combine(mountPath, "elements")
|
|
local filePath = fs.combine(elementsPath, name..".lua")
|
|
|
|
if fs.exists(filePath) then
|
|
ElementManager._elements[name] = {
|
|
class = nil,
|
|
plugins = {},
|
|
loaded = false,
|
|
source = "disk",
|
|
path = filePath
|
|
}
|
|
ElementManager.loadElement(name)
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
if ElementManager._config.allowRemoteLoading and ElementManager._config.remoteSources[name] then
|
|
ElementManager.loadElement(name)
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--- Loads an element by name. This will load the element and apply any plugins to it.
|
|
--- @param name string The name of the element to load
|
|
--- @usage ElementManager.loadElement("Button")
|
|
function ElementManager.loadElement(name)
|
|
if not ElementManager._elements[name] then
|
|
-- Try to auto-load if enabled
|
|
if ElementManager._config.autoLoadMissing then
|
|
local success = ElementManager.tryAutoLoad(name)
|
|
if not success then
|
|
error("Element '"..name.."' not found and could not be auto-loaded")
|
|
end
|
|
else
|
|
error("Element '"..name.."' not found")
|
|
end
|
|
end
|
|
|
|
if not ElementManager._elements[name].loaded then
|
|
local source = ElementManager._elements[name].source or "local"
|
|
local element
|
|
local loadedFromCache = false
|
|
|
|
element = loadFromGlobalCache(name)
|
|
if element then
|
|
loadedFromCache = true
|
|
log.info("Loaded element from _G cache: "..name)
|
|
elseif source == "local" then
|
|
package.path = main.."rom/?"
|
|
element = require(fs.combine("elements", name))
|
|
package.path = defaultPath
|
|
elseif source == "disk" then
|
|
if not ElementManager._config.allowDiskLoading then
|
|
error("Disk loading is disabled for element: "..name)
|
|
end
|
|
element = loadFromDisk(ElementManager._elements[name].path)
|
|
saveToGlobalCache(name, element)
|
|
elseif source == "remote" then
|
|
if not ElementManager._config.allowRemoteLoading then
|
|
error("Remote loading is disabled for element: "..name)
|
|
end
|
|
element = loadFromRemote(ElementManager._elements[name].path)
|
|
saveToGlobalCache(name, element)
|
|
else
|
|
error("Unknown source type: "..source)
|
|
end
|
|
|
|
ElementManager._elements[name] = {
|
|
class = element,
|
|
plugins = element.plugins,
|
|
loaded = true,
|
|
source = loadedFromCache and "cache" or source,
|
|
path = ElementManager._elements[name].path
|
|
}
|
|
|
|
if not loadedFromCache then
|
|
log.debug("Loaded element: "..name.." from "..source)
|
|
end
|
|
|
|
if(ElementManager._plugins[name]~=nil)then
|
|
for _, plugin in pairs(ElementManager._plugins[name]) do
|
|
if(plugin.setup)then
|
|
plugin.setup(element)
|
|
end
|
|
|
|
if(plugin.hooks)then
|
|
for methodName, hooks in pairs(plugin.hooks) do
|
|
local original = element[methodName]
|
|
if(type(original)~="function")then
|
|
error("Element "..name.." does not have a method "..methodName)
|
|
end
|
|
if(type(hooks)=="function")then
|
|
element[methodName] = function(self, ...)
|
|
local result = original(self, ...)
|
|
local hookResult = hooks(self, ...)
|
|
return hookResult == nil and result or hookResult
|
|
end
|
|
elseif(type(hooks)=="table")then
|
|
element[methodName] = function(self, ...)
|
|
if hooks.pre then hooks.pre(self, ...) end
|
|
local result = original(self, ...)
|
|
if hooks.post then hooks.post(self, ...) end
|
|
return result
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for funcName, func in pairs(plugin) do
|
|
if funcName ~= "setup" and funcName ~= "hooks" then
|
|
element[funcName] = func
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Gets an element by name. If the element is not loaded, it will try to load it first.
|
|
--- @param name string The name of the element to get
|
|
--- @return table Element The element class
|
|
function ElementManager.getElement(name)
|
|
if not ElementManager._elements[name] then
|
|
if ElementManager._config.autoLoadMissing then
|
|
local success = ElementManager.tryAutoLoad(name)
|
|
if not success then
|
|
error("Element '"..name.."' not found")
|
|
end
|
|
else
|
|
error("Element '"..name.."' not found")
|
|
end
|
|
end
|
|
|
|
if not ElementManager._elements[name].loaded then
|
|
ElementManager.loadElement(name)
|
|
end
|
|
return ElementManager._elements[name].class
|
|
end
|
|
|
|
--- Gets a list of all elements
|
|
--- @return table ElementList A list of all elements
|
|
function ElementManager.getElementList()
|
|
return ElementManager._elements
|
|
end
|
|
|
|
--- Gets an Plugin API by name
|
|
--- @param name string The name of the API to get
|
|
--- @return table API The API
|
|
function ElementManager.getAPI(name)
|
|
return ElementManager._APIs[name]
|
|
end
|
|
|
|
--- Checks if an element exists (is registered)
|
|
--- @param name string The element name
|
|
--- @return boolean exists Whether the element exists
|
|
function ElementManager.hasElement(name)
|
|
return ElementManager._elements[name] ~= nil
|
|
end
|
|
|
|
--- Checks if an element is loaded
|
|
--- @param name string The element name
|
|
--- @return boolean loaded Whether the element is loaded
|
|
function ElementManager.isElementLoaded(name)
|
|
return ElementManager._elements[name] and ElementManager._elements[name].loaded or false
|
|
end
|
|
|
|
--- Clears the global cache (_G)
|
|
--- @usage ElementManager.clearGlobalCache()
|
|
function ElementManager.clearGlobalCache()
|
|
if _G[ElementManager._config.globalCacheName] then
|
|
_G[ElementManager._config.globalCacheName] = nil
|
|
log.info("Cleared global element cache")
|
|
end
|
|
end
|
|
|
|
--- Gets cache statistics
|
|
--- @return table stats Cache statistics with size and element names
|
|
function ElementManager.getCacheStats()
|
|
if not _G[ElementManager._config.globalCacheName] then
|
|
return {size = 0, elements = {}}
|
|
end
|
|
|
|
local elements = {}
|
|
for name, _ in pairs(_G[ElementManager._config.globalCacheName]) do
|
|
table.insert(elements, name)
|
|
end
|
|
|
|
return {
|
|
size = #elements,
|
|
elements = elements
|
|
}
|
|
end
|
|
|
|
--- Preloads elements into the global cache
|
|
--- @param elementNames table List of element names to preload
|
|
function ElementManager.preloadElements(elementNames)
|
|
for _, name in ipairs(elementNames) do
|
|
if ElementManager._elements[name] and not ElementManager._elements[name].loaded then
|
|
ElementManager.loadElement(name)
|
|
end
|
|
end
|
|
end
|
|
|
|
return ElementManager |