dfhack/library/lua/script-manager.lua

183 lines
6.2 KiB
Lua

local _ENV = mkmodule('script-manager')
local utils = require('utils')
---------------------
-- enabled API
-- for each script that can be loaded as a module, calls cb(script_name, env)
function foreach_module_script(cb, preprocess_script_file_fn)
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
local files = dfhack.filesystem.listdir_recursive(
script_path, nil, false)
if not files then goto skip_path end
for _,f in ipairs(files) do
if f.isdir or not f.path:endswith('.lua') or
f.path:startswith('.git') or
f.path:startswith('test/') or
f.path:startswith('internal/') then
goto continue
end
if preprocess_script_file_fn then
preprocess_script_file_fn(script_path, f.path)
end
local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua'
local ok, script_env = pcall(reqscript, script_name)
if ok then
cb(script_name, script_env)
end
::continue::
end
::skip_path::
end
end
local enabled_map = nil
local function process_script(env_name, env)
local global_name = 'isEnabled'
local fn = env[global_name]
if not fn then return end
if type(fn) ~= 'function' then
dfhack.printerr(
('error registering %s() from "%s": global' ..
' value is not a function'):format(global_name, env_name))
return
end
enabled_map[env_name] = fn
end
function reload(refresh_active_mod_scripts)
enabled_map = utils.OrderedTable()
local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name)
if script_path:find('scripts_modactive') then
local full_path = script_path..'/'..script_name
internal_script = dfhack.internal.scripts[full_path]
if internal_script then
dfhack.internal.scripts[full_path] = nil
end
end
end or nil
foreach_module_script(process_script, force_refresh_fn)
end
local function ensure_loaded()
if not enabled_map then
reload()
end
end
function list()
ensure_loaded()
for name,fn in pairs(enabled_map) do
print(('%21s %-3s'):format(name..':', fn() and 'on' or 'off'))
end
end
---------------------
-- mod paths
-- this perhaps could/should be queried from the Steam API
-- are there any installation configurations where this will be wrong, though?
local WORKSHOP_MODS_PATH = '../../workshop/content/975370/'
local MODS_PATH = 'mods/'
local INSTALLED_MODS_PATH = 'data/installed_mods/'
-- last instance of the same version of the same mod wins, so read them in this
-- order (in increasing order of liklihood that players may have made custom
-- changes to the files)
local MOD_PATH_ROOTS = {WORKSHOP_MODS_PATH, MODS_PATH, INSTALLED_MODS_PATH}
local function get_mod_id_and_version(path)
local idfile = path .. '/info.txt'
local ok, lines = pcall(io.lines, idfile)
if not ok then return end
local id, version
for line in lines do
if not id then
_,_,id = line:find('^%[ID:([^%]]+)%]')
end
if not version then
-- note this doesn't include the closing brace since some people put
-- non-number characters in here, and DF only reads the leading digits
-- as the numeric version
_,_,version = line:find('^%[NUMERIC_VERSION:(%d+)')
end
-- note that we do *not* want to break out of this loop early since
-- lines has to hit EOF to close the file
end
return id, version
end
local function add_mod_paths(mod_paths, id, base_path, subdir)
local sep = base_path:endswith('/') and '' or '/'
local path = ('%s%s%s'):format(base_path, sep, subdir)
if dfhack.filesystem.isdir(path) then
print('indexing mod path: ' .. path)
table.insert(mod_paths, {id=id, path=path})
end
end
function get_mod_paths(installed_subdir, active_subdir)
-- ordered map of mod id -> {handled=bool, versions=map of version -> path}
local mods = utils.OrderedTable()
local mod_paths = {}
-- if a world is loaded, process active mods first, and lock to active version
if active_subdir and dfhack.isWorldLoaded() then
for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do
path = tostring(path.value)
-- skip vanilla "mods"
if not path:startswith(INSTALLED_MODS_PATH) then goto continue end
local id = get_mod_id_and_version(path)
if not id then goto continue end
mods[id] = {handled=true}
add_mod_paths(mod_paths, id, path, active_subdir)
add_mod_paths(mod_paths, id, path, installed_subdir)
::continue::
end
end
-- assemble version -> path maps for all (non-handled) mod source dirs
for _,mod_path_root in ipairs(MOD_PATH_ROOTS) do
local files = dfhack.filesystem.listdir_recursive(mod_path_root, 0)
if not files then goto skip_path_root end
for _,f in ipairs(files) do
if not f.isdir then goto continue end
local id, version = get_mod_id_and_version(f.path)
if not id or not version then goto continue end
local mod = ensure_key(mods, id)
if mod.handled then goto continue end
ensure_key(mod, 'versions')[version] = f.path
::continue::
end
::skip_path_root::
end
-- add paths from most recent version of all not-yet-handled mods
for id,v in pairs(mods) do
if v.handled then goto continue end
local max_version, path
for version,mod_path in pairs(v.versions) do
if not max_version or max_version < version then
path = mod_path
max_version = version
end
end
add_mod_paths(mod_paths, id, path, installed_subdir)
::continue::
end
return mod_paths
end
function get_mod_script_paths()
local paths = {}
for _,v in ipairs(get_mod_paths('scripts_modinstalled', 'scripts_modactive')) do
table.insert(paths, v.path)
end
return paths
end
return _ENV