mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
When updating a mod Hotpatch updates the loaded_mods array but not the global.mods array. This causes events to be sent in a different order on the client/server. Fix by updating global.mods in install_mod.
1350 lines
46 KiB
Lua
1350 lines
46 KiB
Lua
--[[
|
|
|
|
Copyright 2018 Chrisgbk
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
]]
|
|
-- MIT License, https://opensource.org/licenses/MIT
|
|
|
|
-- Hotpatch-MultiMod: a tool to load multiple scenarios side-by-side, with support for both static loading and dynamic loading, as well as run-time patching
|
|
|
|
log('Hotpatch runtime initializing...')
|
|
local util = require 'util'
|
|
|
|
local compat_mode = false -- enable some compatibility settings, which can help some mods load
|
|
local strict_mode = false -- causes hotpatch to hard-stop on mods doing bad things
|
|
|
|
local debug_levels = {['disabled'] = -1, ['severe'] = 1, ['error'] = 2, ['warning'] = 3, ['info'] = 4, ['verbose'] = 5, ['trace'] = 6}
|
|
|
|
-- configuration options; these should be exposed via API one day
|
|
local debug_level = (tonumber(_ENV.debug_settings.level) or debug_levels[_ENV.debug_settings.level]) or 0
|
|
local debug_log_to_console_only = _ENV.debug_settings.log_to_console_only
|
|
local debug_log_to_RCON = _ENV.debug_settings.log_to_RCON --only affects when log_to_console_only is in effect
|
|
local debug_log_on_tick = _ENV.debug_settings.log_on_tick
|
|
|
|
-- convenience object(rcon.print also prints to stdout when called from server console)
|
|
local console = {name = 'Console', admin = true, print = function(...) rcon.print(...) end, color = {1,1,1,1}}
|
|
|
|
-- mapping of events to names for logging
|
|
local event_names = {}
|
|
for k,v in pairs(defines.events) do
|
|
event_names[v] = k
|
|
end
|
|
|
|
-- these represent the mods as statically packaged with the scenario
|
|
local static_mods = {}
|
|
-- static_mods[i] = {}
|
|
-- static_mods[i].name = ''
|
|
-- static_mods[i].version = ''
|
|
-- static_mods[i].code = ''
|
|
-- static_mods[i].files = {}
|
|
|
|
-- installed mods are represented with:
|
|
-- global.mods = {}
|
|
-- global.mods[i] = {}
|
|
-- global.mods[i].name = ''
|
|
-- global.mods[i].version = ''
|
|
-- global.mods[i].code = ''
|
|
-- global.mods[i].files = {}
|
|
-- global.mods[i].global = {}
|
|
|
|
-- loaded/running mods are represented with:
|
|
local loaded_mods = {} -- this holds a reference to the mods internal object
|
|
-- loaded_mods[i] = mod_obj_template
|
|
|
|
-- internal mod object:
|
|
local mod_obj_template = {
|
|
name = '', -- name of the current mod
|
|
version = '',
|
|
env = {}, -- environment of the mod
|
|
loaded = false,
|
|
running = false,
|
|
on_init = nil, -- called once after first install, or when specifically requested to be re-ran (good practice is to never request a re-run)
|
|
on_load = nil, -- called every time the scenario loads from disk
|
|
on_configuration_changed = nil, -- called every time the external mod configuration changes, OR when the hotpatch mod configuration changes
|
|
on_tick = nil, -- cache the on-tick event handler, because it is ID 0, which causes it to be stored in the hash part, which causes a 50% increase in access time
|
|
on_event = {}, -- list of on_event handlers registered
|
|
on_nth_tick = {}, -- list of on_nth_tick handlers registered
|
|
loaded_files = {}, -- list of files loaded by require() that were installed into the virtual file system
|
|
}
|
|
|
|
-- this is for the core factorio libraries; hotpatch MUST load these now, as they cannot be dynamically loaded due to factorio limitation
|
|
local loaded_libraries = {}
|
|
|
|
-- mod installation/uninstallation support functions
|
|
-- These take a mod NAME as a first argument
|
|
local install_mod
|
|
local find_installed_mod
|
|
local install_mod_file
|
|
local uninstall_mod
|
|
|
|
-- mod interaction functions
|
|
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
|
local load_mod
|
|
local find_loaded_mod
|
|
local run_mod
|
|
local reset_mod
|
|
local reset_mod_events
|
|
local register_mod_events
|
|
local unload_mod
|
|
|
|
-- internal callbacks when a mod registers events
|
|
local register_event
|
|
local register_nth_tick
|
|
local register_on_tick
|
|
|
|
-- mod bootstrap functions
|
|
-- These take a LOADED INDEX as a first argument
|
|
local mod_on_init
|
|
local mod_on_load
|
|
local mod_on_configuration_changed
|
|
|
|
-- local event handler to proxy to mods, these call the mods events
|
|
local on_event
|
|
local on_nth_tick
|
|
|
|
local register_all_events
|
|
|
|
-- core handlers
|
|
local on_init -- installs static mods, loads, runs, and calls mod_on_init
|
|
local on_load -- loads, runs, and calls mod_on_load
|
|
local on_configuration_changed -- calls mod_on_configuration_changed
|
|
|
|
-- this is dual-purpose, handles core needs and mod on_tick
|
|
local on_tick
|
|
|
|
-- this installs a new mod statically, only installed during on_init
|
|
-- Installed mod will still be updatable
|
|
local function static_mod(name, version, code, files)
|
|
local mod = {}
|
|
mod.name = name
|
|
mod.version = version
|
|
mod.code = code
|
|
mod.files = {}
|
|
if files then
|
|
for k, v in pairs(files) do
|
|
mod.files[k] = v
|
|
end
|
|
end
|
|
table.insert(static_mods, mod)
|
|
end
|
|
|
|
-- This scenario contains an embedded locale, but it's unavailable until this file finishes loading
|
|
-- ie: calls to log{...} will fail right now, but will work in events
|
|
-- Rseding91 fixed it for 0.17 (https://forums.factorio.com/viewtopic.php?f=23&t=60767)
|
|
-- I'll make it work myself in 0.16 ¯\_(ツ)_/¯
|
|
local static_cfg = [[
|
|
[hotpatch]
|
|
log=@__1__:__2__] __3__
|
|
log-mod=@__1__:__2__][__4__] __3__
|
|
severe=SEVERE ERROR: __1__
|
|
error=ERROR: __1__
|
|
warning=WARNING: __1__
|
|
info=INFO: __1__
|
|
verbose=INFO: __1__
|
|
trace=TRACE: __1__
|
|
|
|
[hotpatch-info]
|
|
logging-enabled=Logging enabled
|
|
metatable-installed=_ENV metatable installed
|
|
complete=__1__ Complete!
|
|
on-init=initializing...
|
|
on-load=loading...
|
|
on-configuration-changed=mod configuration changed...
|
|
installing-included-mods=installing included mods...
|
|
loading-installed-mods=loading installed mods...
|
|
loading-libs=loading Factorio.data.core.lualib...
|
|
loading-library=loading library: __1__
|
|
uninstalling=Uninstalling mod...
|
|
installing=Installing version __1__...
|
|
installing-file=Installing file __1__...
|
|
script-shim=setting up mod script shim...
|
|
setting-env=setting up mod _ENV...
|
|
loading=loading...
|
|
unloading=unloading...
|
|
running=running...
|
|
must-be-admin=You must be an admin to run this command
|
|
remote-installing=installing remote interface
|
|
|
|
[hotpatch-trace]
|
|
nil-var-access=_ENV nil variable access: __1__
|
|
nil-var-assignment=_ENV variable assignment: __1__
|
|
event-registering=registering events...
|
|
on-tick-event-registered=registered on_tick event...
|
|
on-event-registered=registered event __1__...
|
|
on-nth-tick-event-registered=registered nth_tick event __1__...
|
|
event-running=running event __1__...
|
|
nth-tick-event-running=running nth_event __1__...
|
|
mod-on-init=running on_init...
|
|
mod-on-load=running on_load...
|
|
mod-on-configuration-changed=running on_configuration_changed...
|
|
adding-event=adding event __1__
|
|
adding-nth-tick-event=adding nth_tick event __1__
|
|
caching-event=caching event: __1__...
|
|
caching-nth-tick-event=caching nth_tick event: __1__...
|
|
event-processing=processing event __1__...
|
|
nth-tick-event-processing=processing nth_tick event __1__...
|
|
nth-tick-handler-added=added nth_tick handler: __1__
|
|
nth-tick-handler-removed=removed nth_tick handler: __1__
|
|
on-tick-handler-added=added on_tick handler
|
|
on-tick-handler-removed=removed on_tick handler
|
|
on-event-handler-added=added event handler: __1__
|
|
on-event-handler-removed=removed event handler: __1__
|
|
cached-load-require=loading cached require'd file: __1__...
|
|
load-require=loading require'd file: __1__...
|
|
load-core-lib=loading from Factorio.data.core.lualib: __1__...
|
|
|
|
[hotpatch-warning]
|
|
contains-comments=mod code contains comments!
|
|
contains-comments-no-lf=mod contains comments and no linefeed!
|
|
contains-comments-console=comments from console will comment out the entire code!
|
|
reset-events-not-running=tried to reset events for mod that isn't running!
|
|
remote-interface-exists=remote interface __1__ already exists, removing...
|
|
command-exists=command __1__ already exists, removing...
|
|
already-loaded=mod already loaded
|
|
reinitializing=reinitializing...
|
|
already-exists=mod already exists
|
|
reinstalling=reinstalling mod in-place
|
|
|
|
[hotpatch-error]
|
|
invalid-API-access=Invalid API access: __1__
|
|
not-installed=mod not installed, cannot install file for mod that does not exist
|
|
compilation-failed=compilation failed for mod
|
|
execution-failed=execution failed for mod
|
|
|
|
[test-pluralization]
|
|
test=__1:(^1$)=singular;(^[2-9]$)=plural single digit;([1-2][0-9]$)=plural ends with double digit <30;(.*)=fallback case%; plural with embedded %;;__
|
|
]]
|
|
|
|
local function build_locale(ini)
|
|
local t = {}
|
|
local section = ''
|
|
local temp
|
|
|
|
-- line must end with a linefeed - single line file with no LF will fail
|
|
if not ini:match('[\r\n]+$') then
|
|
ini = ini .. '\n'
|
|
end
|
|
|
|
-- for each non-empty line do
|
|
local key, value
|
|
for l in ini:gmatch('[\r\n]*(.-)[\r\n]+') do
|
|
-- header?
|
|
temp = l:match('^%[(.-)%].*$')
|
|
if temp then
|
|
section = temp
|
|
temp = nil
|
|
else
|
|
--key=value
|
|
key, value = l:match('^(.-)=(.+)$')
|
|
t[table.concat{section, '.', key}] = value
|
|
end
|
|
end
|
|
return t
|
|
end
|
|
|
|
local static_locale = build_locale(static_cfg)
|
|
|
|
local function escape(s)
|
|
return (s:gsub('([%^%$%(%)%%%.%[%]%*%+%-%?])', '%%%1'))
|
|
end
|
|
|
|
local function unescape(s)
|
|
return (s:gsub('(%%)', ''))
|
|
end
|
|
|
|
local function static_translate(t, recursive)
|
|
-- only translate tables
|
|
if type(t) ~= 'table' then return t end
|
|
-- only translate tables that have a string as the first item
|
|
local k = t[1]
|
|
if type(k) ~= 'string' then return t end
|
|
|
|
-- make a copy, don't destroy the original table, after we copy we can translate in place
|
|
if not recursive then
|
|
t = table.deepcopy(t)
|
|
end
|
|
-- translate any arguments as well
|
|
local v
|
|
for i = 2, #t do
|
|
v = t[i]
|
|
if type(v) == 'table' then
|
|
t[i] = static_translate(v, true)
|
|
end
|
|
end
|
|
-- special case: whitespace token causes concatenation with that token
|
|
-- slightly better than factorio, where only '' is supported
|
|
if k:find('^%s*$') then table.remove(t, 1) return table.concat(t, k) end
|
|
local pattern = static_locale[k]
|
|
-- if not translatable return normal table ref; factorio does following instead:
|
|
-- if not pattern then return 'Unknown key: ' .. k end
|
|
-- by returning the table we pass off to factorio runtime translation, where available
|
|
if not pattern then return t end
|
|
-- substitution of parameters: use literal value of parameter n
|
|
-- __n__
|
|
local result = (pattern:gsub('__(%d+)__', function(s) return tostring(t[tonumber(s)+1]) end))
|
|
|
|
-- re-substitution engine: match value of parameter n to provide additional translation; use for pluralization
|
|
-- __n:(pattern-1)=substitution-1;(pattern-2)=substitution-2;...(pattern-i)=substitution-i;__
|
|
for n, p in result:gmatch('__(%d+)(:.-;)__') do
|
|
for x, y in p:gmatch('%((.-)%)=(.-[^%%]);') do
|
|
if t[tonumber(n)+1]:match(x) then
|
|
result = result:gsub(table.concat{'__', n, escape(p), '__'}, unescape(y))
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
-- override print to make it support our translation efforts
|
|
-- still doesn't support Factorio locales, because devs don't patch it like I do
|
|
-- this means unknown keys will be printed as 'table: 0x...'
|
|
local real_print = print
|
|
local print = function(...)
|
|
if select('#', ...) == 1 then
|
|
real_print(static_translate(...))
|
|
else
|
|
local t = table.pack(...)
|
|
table.insert(t, 1, '\t')
|
|
real_print(static_translate(t))
|
|
end
|
|
end
|
|
|
|
-- override log to make it support our translation efforts
|
|
-- any unknown keys are passed to Factorio to translate
|
|
|
|
local hidden = load([===[
|
|
local real_log = log; local static_translate = select(1, ...); local log = function(...); if select('#', ...) == 1 then real_log(static_translate(...)) else local t = table.pack(...); table.insert(t, 1, '\t'); real_log(static_translate(t)) end end return log
|
|
]===], '[HOTPATCH')
|
|
local hidden_log = hidden(static_translate)
|
|
|
|
-- logs localized data
|
|
local function debug_log(message, mod_name, stack_level)
|
|
if debug_level > -1 then
|
|
if not stack_level then stack_level = 2 end
|
|
local di = debug.getinfo(stack_level)
|
|
local line = di.currentline
|
|
local file = (di.source:gsub('%@.*/', ''))
|
|
local class = 'hotpatch.info'
|
|
local log_type = (mod_name and 'hotpatch.log-mod') or 'hotpatch.log'
|
|
local severity
|
|
if type(message) == 'table' then
|
|
severity = message[1]:match('.-%-([^%-]*)%.')
|
|
if not severity then
|
|
severity = message[1]:match('.-%-.-%-(.*)%.')
|
|
if not severity then
|
|
severity = 'always'
|
|
end
|
|
end
|
|
class = table.concat{'hotpatch', '.', severity}
|
|
else
|
|
severity = 'always'
|
|
end
|
|
local level = ((severity == 'always') and 0) or debug_levels[severity]
|
|
if debug_level >= level then
|
|
if debug_log_to_console_only then
|
|
if debug_log_to_RCON then
|
|
rcon.print{log_type, file, line, {class, message}, mod_name}
|
|
end
|
|
print{log_type, file, line, {class, message}, mod_name}
|
|
|
|
else
|
|
hidden_log{log_type, file, line, {class, message}, mod_name}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
debug_log({'hotpatch-info.logging-enabled'})
|
|
|
|
-- track _ENV accesses
|
|
setmetatable(_ENV, {
|
|
__index = function(_, k)
|
|
debug_log({'hotpatch-trace.nil-var-access', k}, nil, 3)
|
|
return nil
|
|
end,
|
|
__newindex = function(t, k, v)
|
|
debug_log({'hotpatch-trace.nil-var-assignment', k}, nil, 3)
|
|
rawset(t,k,v)
|
|
end,
|
|
__metatable = false
|
|
})
|
|
|
|
debug_log({'hotpatch-info.metatable-installed'})
|
|
|
|
-- load all possible libraries
|
|
debug_log({'hotpatch-info.loading-libs'})
|
|
|
|
local libraries = {
|
|
'camera',
|
|
'flying_tags',
|
|
'math3d',
|
|
'mod-gui',
|
|
'noise',
|
|
'production-score',
|
|
'silo-script',
|
|
'story',
|
|
'util',
|
|
}
|
|
|
|
for k, v in pairs(libraries) do
|
|
debug_log({'hotpatch-info.loading-library', v})
|
|
loaded_libraries[k] = require(v)
|
|
end
|
|
|
|
debug_log({'hotpatch-info.complete', {'hotpatch-info.loading-libs'}})
|
|
|
|
find_installed_mod = function(mod_name)
|
|
local mod
|
|
local mods = global.mods
|
|
for i = 1, #mods do
|
|
mod = mods[i]
|
|
if mod.name == mod_name then
|
|
return i
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
find_loaded_mod = function(mod_name)
|
|
local mod
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
if mod.name == mod_name then
|
|
return i
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
uninstall_mod = function(mod_name)
|
|
local index = find_installed_mod(mod_name)
|
|
if not index then
|
|
-- TODO: notify that mod doesn't exist
|
|
return
|
|
end
|
|
local loaded_index = find_loaded_mod(mod_name)
|
|
|
|
if loaded_index then
|
|
unload_mod(loaded_index)
|
|
end
|
|
|
|
|
|
debug_log({'hotpatch-info.uninstalling', mod_name})
|
|
|
|
table.remove(global.mods, index)
|
|
end
|
|
|
|
install_mod_file = function(mod_name, mod_file, mod_file_code)
|
|
local index = find_installed_mod(mod_name)
|
|
if not index then
|
|
debug_log({'hotpatch-error.not-installed'})
|
|
return
|
|
end
|
|
local mod = global.mods[index]
|
|
|
|
debug_log({'hotpatch-info.installing-file', mod_file}, mod_name)
|
|
|
|
mod_file = mod_file:gsub('/', '.')
|
|
mod_file = mod_file:gsub('\\', '.')
|
|
mod_file_code = mod_file_code:gsub('\t', ' ')
|
|
mod.files[mod_file] = mod_file_code
|
|
end
|
|
|
|
install_mod = function(mod_name, mod_version, mod_code, mod_files)
|
|
local index = find_installed_mod(mod_name)
|
|
local mod = {}
|
|
if index then
|
|
debug_log('attempt to install mod that is already installed, reinstalling: ' .. mod_name)
|
|
mod = table.remove(global.mods, index)
|
|
table.insert(global.mods, mod)
|
|
else
|
|
--next free index
|
|
table.insert(global.mods, mod)
|
|
end
|
|
debug_log({'hotpatch-info.installing', mod_version}, mod_name)
|
|
if mod_code:find('--', 1, true) then
|
|
|
|
debug_log({'hotpatch-warning.contains-comments'}, mod_name)
|
|
if not mod_code:find("\n", 1, true) then
|
|
debug_log({'hotpatch-warning.contains-comments-no-lf'}, mod_name)
|
|
end
|
|
debug_log({'hotpatch-warning.contains-comments-console'}, mod_name)
|
|
end
|
|
|
|
mod_code = mod_code:gsub('\t', ' ')
|
|
|
|
mod.name = mod_name
|
|
mod.files = mod.files or {}
|
|
mod.code = mod_code
|
|
mod.version = mod_version
|
|
mod.global = mod.global or {}
|
|
|
|
if mod_files then
|
|
for k,v in pairs(mod_files) do
|
|
install_mod_file(mod_name, k, v)
|
|
end
|
|
end
|
|
end
|
|
|
|
load_mod = function(installed_index)
|
|
local mod = global.mods[installed_index]
|
|
local mod_name = mod.name
|
|
if mod.code then
|
|
-- TODO: integrity check, verify each mods global table to detect guaranteed desync mods
|
|
local loaded_index = find_loaded_mod(mod_name)
|
|
if loaded_index then
|
|
--TODO notify that mod was already loaded
|
|
unload_mod(loaded_index)
|
|
end
|
|
|
|
local mod_obj = {
|
|
name = mod_name, -- name of the current mod
|
|
version = mod.version,
|
|
env = {}, -- environment of the mod
|
|
loaded = false,
|
|
running = false,
|
|
on_init = nil, -- called once after first install, or when specifically requested to be re-ran (good practice is to never request a re-run)
|
|
on_load = nil, -- called every time the scenario loads from disk
|
|
on_configuration_changed = nil, -- called every time the external mod configuration changes, OR when the hotpatch mod configuration changes
|
|
on_tick = nil, -- cache the on-tick event handler, because it is ID 0, which causes it to be stored in the hash part, which causes a 50% increase in access time
|
|
on_event = {}, -- list of on_event handlers registered
|
|
on_nth_tick = {}, -- list of on_nth_tick handlers registered
|
|
loaded_files = {}, -- list of files loaded by require() that were installed into the virtual file system
|
|
}
|
|
|
|
debug_log({'hotpatch-info.script-shim'}, mod_name)
|
|
--mods private script table/shim
|
|
local mod_script = {}
|
|
|
|
mod_script.on_init = function(f)
|
|
mod_obj.on_init = f
|
|
end
|
|
mod_script.on_load = function(f)
|
|
mod_obj.on_load = f
|
|
end
|
|
mod_script.on_configuration_changed = function(f)
|
|
mod_obj.on_configuration_changed = f
|
|
end
|
|
if not compat_mode then
|
|
mod_script.on_event = function(event, f)
|
|
if event == defines.events.on_tick then
|
|
mod_obj.on_tick = f
|
|
else
|
|
mod_obj.on_event[event] = f
|
|
end
|
|
end
|
|
mod_script.on_nth_tick = function(tick, f)
|
|
if tick then
|
|
if type(tick) == 'table' then
|
|
for _, v in pairs(tick) do
|
|
mod_script.on_nth_tick(v, f)
|
|
end
|
|
return
|
|
end
|
|
mod_obj.on_nth_tick[tick] = f
|
|
else
|
|
mod_obj.on_nth_tick = {}
|
|
end
|
|
end
|
|
else
|
|
mod_script.on_event = function(event, f)
|
|
if event == defines.events.on_tick then
|
|
mod_obj.on_tick = f
|
|
if mod_obj.running then
|
|
register_on_tick(mod_name)
|
|
end
|
|
else
|
|
mod_obj.on_event[event] = f
|
|
if mod_obj.running then
|
|
register_event(mod_name, event)
|
|
end
|
|
end
|
|
end
|
|
mod_script.on_nth_tick = function(tick, f)
|
|
if tick then
|
|
if type(tick) == 'table' then
|
|
for _, v in pairs(tick) do
|
|
mod_script.on_nth_tick(v, f)
|
|
end
|
|
return
|
|
end
|
|
mod_obj.on_nth_tick[tick] = f
|
|
if mod_obj.running then
|
|
register_nth_tick(mod_name, tick)
|
|
end
|
|
else
|
|
local mod_on_nth_tick = mod_obj.on_nth_tick
|
|
mod_obj.on_nth_tick = {}
|
|
if mod_obj.running then
|
|
for _, v in pairs(mod_on_nth_tick) do
|
|
register_nth_tick(mod_name, v)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
mod_script.generate_event_name = function()
|
|
return script.generate_event_name()
|
|
end
|
|
mod_script.get_event_handler = function(event)
|
|
return mod_obj.on_event[event]
|
|
end
|
|
mod_script.raise_event = function(event, table)
|
|
script.raise_event(event, table)
|
|
end
|
|
--TODO: replace these with mod-provided versions, so multi-mod aware softmods can easily detect other loaded softmods
|
|
mod_script.get_event_order = function()
|
|
return script.get_event_order()
|
|
end
|
|
mod_script.mod_name = function()
|
|
return script.mod_name()
|
|
end
|
|
|
|
debug_log({'hotpatch-info.setting-env'}, mod_name)
|
|
-- mods private env
|
|
local env = mod_obj.env
|
|
-- mods private package
|
|
local pack = {}
|
|
-- mods private package.loaded
|
|
local loaded = {}
|
|
-- copy the current environment
|
|
for k,v in pairs(_ENV) do
|
|
env[k] = v
|
|
end
|
|
-- copy package.loaded
|
|
for k,v in pairs(_ENV.package.loaded) do
|
|
loaded[k] = v
|
|
end
|
|
loaded._G = env
|
|
|
|
-- so many ways to escape sandboxes...
|
|
|
|
for k,v in pairs(_ENV.package) do
|
|
pack[k] = v
|
|
end
|
|
pack.loaded = loaded
|
|
env.package = pack
|
|
loaded.package = pack
|
|
|
|
env.script = mod_script
|
|
env.global = mod.global
|
|
env._G = env
|
|
|
|
env['remote'] = {
|
|
add_interface = function(name, functions)
|
|
if remote.interfaces[name] then
|
|
debug_log({'hotpatch-warning.remote-interface-exists', name}, mod_name)
|
|
remote.remove_interface(name)
|
|
end
|
|
remote.add_interface(name, functions)
|
|
end,
|
|
remove_interface = function(name)
|
|
return remote.remove_interface(name)
|
|
end,
|
|
call = function(...)
|
|
return remote.call(...)
|
|
end,
|
|
interfaces = setmetatable({}, {
|
|
__index = function(_, k) return remote.interfaces[k] end,
|
|
__pairs = function(t) local function iter(t, k) local v; k, v = next(remote.interfaces, k); if v then return k, t[k] end; end; return iter, t, nil end
|
|
})
|
|
}
|
|
env['commands'] = {
|
|
add_command = function(name, help, func)
|
|
if commands.commands[name] then
|
|
debug_log({'hotpatch-warning.command-exists', name}, mod_name)
|
|
commands.remove_command(name)
|
|
end
|
|
commands.add_command(name, help, func)
|
|
end,
|
|
remove_command = function(name)
|
|
return commands.remove_command(name)
|
|
end,
|
|
commands = setmetatable({}, {
|
|
__index = function(_, k) return commands.commands[k] end,
|
|
__pairs = function(t) local function iter(t, k) local v; k, v = next(commands.commands, k); if v then return k, t[k] end; end; return iter, t, nil end
|
|
}),
|
|
game_commands = setmetatable({}, {
|
|
__index = function(_, k) return commands.game_commands[k] end,
|
|
__pairs = function(t) local function iter(t, k) local v; k, v = next(commands.game_commands, k); if v then return k, t[k] end; end; return iter, t, nil end
|
|
})
|
|
}
|
|
|
|
|
|
env.require = function(path)
|
|
-- I blame Nexela for this
|
|
path = path:gsub('/', '.')
|
|
path = path:gsub('\\', '.')
|
|
local alt_path = ''
|
|
if env.package._current_path_in_package then
|
|
alt_path = env.package._current_path_in_package .. path
|
|
end
|
|
if mod_obj.loaded_files[path] then
|
|
debug_log({'hotpatch-trace.cached-load-require', path}, mod_name)
|
|
return mod_obj.loaded_files[path]
|
|
elseif mod_obj.loaded_files[alt_path] then
|
|
debug_log({'hotpatch-trace.cached-load-require', alt_path}, mod_name)
|
|
return mod_obj.loaded_files[alt_path]
|
|
else
|
|
local oldbase = env.package._current_path_in_package
|
|
env.package._current_path_in_package = path:match('.+%..+%.')
|
|
if not env.package._current_path_in_package then
|
|
env.package._current_path_in_package = path:match('.+%.')
|
|
end
|
|
local file = global.mods[installed_index].files[path]
|
|
if file then
|
|
debug_log({'hotpatch-trace.load-require', path}, mod_name)
|
|
local code, err = load(file, '[' .. mod_name .. '] ' .. path .. '.lua', 'bt', env)
|
|
if code then
|
|
local result = code()
|
|
mod_obj.loaded_files[path] = result or true
|
|
env.package._current_path_in_package = oldbase
|
|
return mod_obj.loaded_files[path]
|
|
else
|
|
debug_log(err, nil, 3)
|
|
error(err)
|
|
end
|
|
end
|
|
file = global.mods[installed_index].files[alt_path]
|
|
if file then
|
|
debug_log({'hotpatch-trace.load-require', alt_path}, mod_name)
|
|
local code, err = load(file, '[' .. mod_name .. '] ' .. alt_path .. '.lua', 'bt', env)
|
|
if code then
|
|
local result = code()
|
|
mod_obj.loaded_files[alt_path] = result or true
|
|
env.package._current_path_in_package = oldbase
|
|
return mod_obj.loaded_files[alt_path]
|
|
else
|
|
debug_log(err, nil, 3)
|
|
error(err)
|
|
end
|
|
end
|
|
|
|
debug_log({'hotpatch-trace.load-core-lib', path}, mod_name)
|
|
env.package._current_path_in_package = oldbase
|
|
local lib = package.loaded[path]
|
|
if not lib then
|
|
debug_log({'hotpatch-error.path-not-found', path}, mod_name, 3)
|
|
error(path .. ' not found')
|
|
end
|
|
return lib
|
|
end
|
|
end
|
|
|
|
env['load'] = function(l, s, m, e)
|
|
return load(l, s, m, e or env)
|
|
end
|
|
env['loadstring'] = env['load']
|
|
|
|
|
|
env['game'] = setmetatable({}, {
|
|
__index = function(_, k) return game[k] end,
|
|
__pairs = function(t) local function iter(t, k) local v; k, v = next(game, k); if v then return k, t[k] end; end; return iter, t, nil end
|
|
})
|
|
|
|
local mt = {}
|
|
mt.__index = function(_, k)
|
|
debug_log({'hotpatch-trace.nil-var-access', k}, nil, 3)
|
|
return nil
|
|
end
|
|
mt.__newindex = function(t, k, v)
|
|
debug_log({'hotpatch-trace.nil-var-assignment', k}, nil, 3)
|
|
rawset(t,k,v)
|
|
end
|
|
-- Don't let mods break this
|
|
mt.__metatable = false
|
|
setmetatable(env, mt)
|
|
|
|
--load/run code
|
|
debug_log({'hotpatch-info.loading'}, mod_name)
|
|
|
|
local mod_code, message = load(mod.code, '[' .. mod_name .. '] control.lua', 'bt', env)
|
|
if not mod_code then
|
|
debug_log({'hotpatch-error.compilation-failed'}, mod_name)
|
|
if game and game.player then
|
|
game.player.print(message)
|
|
end
|
|
debug_log(message, mod_name)
|
|
return false
|
|
end
|
|
|
|
mod_obj.code = mod_code
|
|
mod_obj.loaded = true
|
|
table.insert(loaded_mods, mod_obj)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
unload_mod = function(loaded_index)
|
|
local mod = loaded_mods[loaded_index]
|
|
if not mod then
|
|
--TODO notify that mod isn't loaded
|
|
return
|
|
end
|
|
local mod_name = mod.name
|
|
-- TODO
|
|
-- stop mod running, unregister handlers from being called
|
|
debug_log({'hotpatch-info.unloading'}, mod_name)
|
|
-- TODO:
|
|
mod.loaded = false
|
|
mod.running = false
|
|
table.remove(loaded_mods, loaded_index)
|
|
end
|
|
|
|
|
|
--TODO: pretty much all of this routine
|
|
-- This should unregister events and clear the globals
|
|
reset_mod = function(loaded_index)
|
|
local new_global = {}
|
|
local mod = loaded_mods[loaded_index]
|
|
mod.mod_env.global = new_global
|
|
local install_index = find_installed_mod(mod.name)
|
|
mod = global.mods[install_index]
|
|
mod.global = new_global
|
|
|
|
reset_mod_events(loaded_index)
|
|
end
|
|
|
|
reset_mod_events = function(loaded_index)
|
|
--local loaded_index = find_loaded_mod(mod_name)
|
|
local loaded_mod = loaded_mods[loaded_index]
|
|
|
|
if not loaded_mod then
|
|
--debug_log({'hotpatch-warning.reset-events-not-running'}, mod_name)
|
|
else
|
|
loaded_mod.on_event = {}
|
|
loaded_mod.on_nth_tick = {}
|
|
loaded_mod.on_init = nil
|
|
loaded_mod.on_load = nil
|
|
loaded_mod.on_configuration_changed = nil
|
|
loaded_mod.on_tick = nil
|
|
register_all_events()
|
|
end
|
|
end
|
|
|
|
local wrap_table
|
|
wrap_table = function(t, path)
|
|
local mt = {
|
|
__index = function(_, k)
|
|
local v = rawget(t, k)
|
|
if type(v) == 'table' then
|
|
if not v.__self then
|
|
local p = (path or 'global') .. '[' .. tostring(k) .. ']'
|
|
return wrap_table(v, p)
|
|
end
|
|
end
|
|
return v
|
|
end,
|
|
__newindex = function(_, k, v)
|
|
local k_type = type(k)
|
|
local wrap = ((k_type == 'string') and '"') or ''
|
|
local p = (path or 'global') .. '[' .. wrap .. tostring(k) .. wrap .. ']'
|
|
debug_log('WARN: Assignment to global table when none was expected: '.. p, nil, 3)
|
|
rawset(t, k, v)
|
|
end
|
|
}
|
|
return setmetatable({}, mt)
|
|
end
|
|
|
|
|
|
run_mod = function(loaded_index)
|
|
local mod = loaded_mods[loaded_index]
|
|
if mod then
|
|
local mod_name = mod.name
|
|
local old_global = mod.env.global
|
|
mod.env.global = wrap_table(old_global)
|
|
|
|
debug_log({'hotpatch-info.running'}, mod_name)
|
|
|
|
local success, result = xpcall(mod.code, debug.traceback)
|
|
if not success then
|
|
debug_log({'hotpatch-error.execution-failed'}, mod_name)
|
|
debug_log(result, mod_name)
|
|
--local caller = (game and game.player) or console
|
|
if game and game.player then
|
|
game.player.print(result)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
mod.env.global = old_global
|
|
old_global = nil
|
|
|
|
mod.running = true
|
|
debug_log({'hotpatch-info.complete', {'hotpatch-info.running'}}, mod_name)
|
|
|
|
--load complete, start notifying on event subscriptions
|
|
if not compat_mode then
|
|
mod.env.script.on_event = function(event, f)
|
|
if event == defines.events.on_tick then
|
|
mod.on_tick = f
|
|
register_on_tick(mod_name)
|
|
else
|
|
mod.on_event[event] = f
|
|
register_event(mod_name, event)
|
|
end
|
|
end
|
|
mod.env.script.on_nth_tick = function(tick, f)
|
|
if tick then
|
|
if type(tick) == 'table' then
|
|
for _, v in pairs(tick) do
|
|
mod.env.script.on_nth_tick(v, f)
|
|
end
|
|
return
|
|
end
|
|
mod.on_nth_tick[tick] = f
|
|
register_nth_tick(mod_name, tick)
|
|
else
|
|
local mod_on_nth_tick = mod.on_nth_tick
|
|
mod.on_nth_tick = {}
|
|
for _, v in pairs(mod_on_nth_tick) do
|
|
register_nth_tick(mod_name, v)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Note: might be able to optimize this a bit
|
|
-- event handlers to call into mods requested event handlers
|
|
on_event = function(event)
|
|
local event_name = (event_names[event.name] or event.name)
|
|
local f
|
|
debug_log({'hotpatch-trace.event-processing', event_name})
|
|
local mod
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
f = mod.on_event[event.name]
|
|
if f then
|
|
debug_log({'hotpatch-trace.event-running', event_name}, mod.name)
|
|
f(event)
|
|
end
|
|
end
|
|
end
|
|
|
|
if debug_log_on_tick then
|
|
on_nth_tick = function(event)
|
|
local tick = event.nth_tick
|
|
local f
|
|
debug_log({'hotpatch-trace.nth-tick-event-processing', tick})
|
|
local mod
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
f = mod.on_nth_tick[event.nth_tick]
|
|
if f then
|
|
debug_log({'hotpatch-trace.nth-tick-event-running', tick}, mod.name)
|
|
f(event)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
on_nth_tick = function(event)
|
|
local tick = event.nth_tick
|
|
local f
|
|
local mod
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
f = mod.on_nth_tick[event.nth_tick]
|
|
if f then
|
|
debug_log({'hotpatch-trace.nth-tick-event-running', tick}, mod.name)
|
|
f(event)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
register_all_events = function()
|
|
--unregister all events
|
|
script.on_event(defines.events, nil)
|
|
script.on_nth_tick(nil, nil)
|
|
--re-register all mod events
|
|
for i = 1, #loaded_mods do
|
|
register_mod_events(i)
|
|
end
|
|
end
|
|
|
|
register_mod_events = function(loaded_index)
|
|
|
|
local mod = loaded_mods[loaded_index]
|
|
local mod_name = mod.name
|
|
debug_log({'hotpatch-trace.event-registering'}, mod_name)
|
|
if mod.on_tick then
|
|
debug_log({'hotpatch-trace.on-tick-event-registered'}, mod_name)
|
|
script.on_event(defines.events.on_tick, on_tick)
|
|
end
|
|
for k,_ in pairs(mod.on_event) do
|
|
local event_name = (event_names[k] or k)
|
|
debug_log({'hotpatch-trace.on-event-registered', event_name}, mod_name)
|
|
script.on_event(k, on_event)
|
|
end
|
|
for k,_ in pairs(mod.on_nth_tick) do
|
|
debug_log({'hotpatch-trace.on-nth-tick-event-registered', k}, mod_name)
|
|
script.on_nth_tick(k, on_nth_tick)
|
|
end
|
|
end
|
|
|
|
mod_on_init = function(loaded_index)
|
|
local mod = loaded_mods[loaded_index]
|
|
|
|
if mod then
|
|
debug_log({'hotpatch-trace.mod-on-init'}, mod.name)
|
|
if mod.on_init then
|
|
local success, result = xpcall(mod.on_init, debug.traceback)
|
|
if not success then
|
|
debug_log({'hotpatch-error.on-init-failed'}, mod_name)
|
|
debug_log(result, mod_name)
|
|
return false
|
|
end
|
|
end
|
|
register_mod_events(loaded_index)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
mod_on_load = function(loaded_index)
|
|
local mod = loaded_mods[loaded_index]
|
|
|
|
if mod then
|
|
debug_log({'hotpatch-trace.mod-on-load'}, mod.name)
|
|
if mod.on_load then
|
|
local old_global = mod.env.global
|
|
mod.env.global = wrap_table(table.deepcopy(old_global))
|
|
local success, result = xpcall(mod.on_load, debug.traceback)
|
|
if not success then
|
|
debug_log({'hotpatch-error.on-load-failed'}, mod_name)
|
|
debug_log(result, mod_name)
|
|
return false
|
|
end
|
|
mod.env.global = old_global
|
|
|
|
end
|
|
register_mod_events(loaded_index)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
mod_on_configuration_changed = function(loaded_index, config)
|
|
local mod = loaded_mods[loaded_index]
|
|
|
|
if mod then
|
|
debug_log({'hotpatch-trace.mod-on-configuration-changed'}, mod.name)
|
|
if mod.on_configuration_changed then
|
|
local success, result = xpcall(mod.on_configuration_changed, debug.traceback, config)
|
|
if not success then
|
|
debug_log({'hotpatch-error.on-configuration-changed-failed'}, mod_name)
|
|
debug_log(result, mod_name)
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- callbacks from mods to tell hotpatch when to enable handlers
|
|
|
|
register_on_tick = function(mod_name)
|
|
local found_event
|
|
--
|
|
local mod
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
local f = mod.on_tick
|
|
if f then
|
|
found_event = true
|
|
break
|
|
end
|
|
end
|
|
debug_log({'hotpatch-trace.on-tick-event-registered'}, mod_name)
|
|
if found_event then
|
|
debug_log({'hotpatch-trace.on-tick-handler-added'})
|
|
else
|
|
debug_log({'hotpatch-trace.on-tick-handler-removed'})
|
|
end
|
|
end
|
|
|
|
register_nth_tick = function(mod_name, nth_tick)
|
|
local found_event
|
|
--
|
|
local mod
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
local f = mod.on_nth_tick[nth_tick]
|
|
if f then
|
|
found_event = true
|
|
break
|
|
end
|
|
end
|
|
debug_log({'hotpatch-trace.on-nth-tick-event-registered', nth_tick}, mod_name)
|
|
if found_event then
|
|
debug_log({'hotpatch-trace.nth-tick-handler-added', nth_tick})
|
|
script.on_nth_tick(nth_tick, on_nth_tick)
|
|
else
|
|
debug_log({'hotpatch-trace.nth-tick-handler-removed', nth_tick})
|
|
script.on_nth_tick(nth_tick, nil)
|
|
end
|
|
end
|
|
|
|
register_event = function(mod_name, event_name)
|
|
local found_event
|
|
local mod
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
local f = mod.on_event[event_name]
|
|
if f then found_event = true break end
|
|
end
|
|
debug_log({'hotpatch-trace.on-event-registered', (event_names[event_name] or event_name)}, mod_name)
|
|
if found_event then
|
|
if script.get_event_handler(event_name) then
|
|
-- handler already installed
|
|
return
|
|
else
|
|
debug_log({'hotpatch-trace.on-event-handler-added', (event_names[event_name] or event_name)})
|
|
script.on_event(event_name, on_event)
|
|
end
|
|
else
|
|
debug_log({'hotpatch-trace.on-event-handler-removed', (event_names[event_name] or event_name)})
|
|
script.on_event(event_name, nil)
|
|
end
|
|
end
|
|
|
|
-- Core registration
|
|
|
|
on_init = function()
|
|
hidden = load([===[
|
|
return function(...) log(...) end
|
|
]===], '[HOTPATCH')
|
|
hidden_log = hidden()
|
|
debug_log({'hotpatch-info.on-init'})
|
|
--juuuuust in case
|
|
global.mods = global.mods or {}
|
|
|
|
debug_log({'hotpatch-info.installing-included-mods'})
|
|
local mod
|
|
for i = 1, #static_mods do
|
|
mod = static_mods[i]
|
|
install_mod(mod.name, mod.version, mod.code, mod.files)
|
|
end
|
|
-- TODO: fix mod error loading behaviours
|
|
for i = 1, #global.mods do
|
|
load_mod(i)
|
|
end
|
|
|
|
for i = 1, #loaded_mods do
|
|
run_mod(i)
|
|
mod_on_init(i)
|
|
end
|
|
debug_log({'hotpatch-info.complete', {'hotpatch-info.installing-included-mods'}})
|
|
debug_log({'hotpatch-info.complete', {'hotpatch-info.on-init'}})
|
|
end
|
|
|
|
on_load = function()
|
|
hidden = load([===[
|
|
return function(...) log(...) end
|
|
]===], '[HOTPATCH')
|
|
hidden_log = hidden()
|
|
debug_log({'hotpatch-info.on-load'})
|
|
debug_log({'hotpatch-info.loading-installed-mods'})
|
|
|
|
if global.globals then
|
|
error('Upgrading from Hotpatch 1.0.X to 1.1.0 is not currently supported!')
|
|
|
|
-- untested migration: use a tick to do the migration
|
|
script.on_event(defines.events.on_tick, function(e)
|
|
global.mods = {}
|
|
local mod
|
|
for k, v in pairs(global.mod_version) do
|
|
mod = {}
|
|
mod.name = k
|
|
mod.version = v
|
|
mod.code = global.mod_code[k]
|
|
mod.files = global.mod_files[k]
|
|
mod.global = global.globals[k]
|
|
table.insert(global.mods, mod)
|
|
end
|
|
global.mod_version = nil
|
|
global.mod_code = nil
|
|
global.mod_files = nil
|
|
global.globals = nil
|
|
|
|
on_load() -- jump back into loading
|
|
on_tick(e) -- need to test if factorio auto calls this or not after assigning the handler
|
|
script.on_event(defines.events.on_tick, on_tick) -- restore old on_tick handler
|
|
end)
|
|
return
|
|
end
|
|
|
|
-- load installed mods
|
|
for i = 1, #global.mods do
|
|
load_mod(i)
|
|
end
|
|
|
|
-- run mods which loaded successfully
|
|
local failed_mods = {}
|
|
--log('before')
|
|
--log(serpent.block(global))
|
|
|
|
for i = 1, #loaded_mods do
|
|
if run_mod(i) then
|
|
if not mod_on_load(i) then
|
|
table.insert(failed_mods, i)
|
|
end
|
|
else
|
|
table.insert(failed_mods, i)
|
|
end
|
|
end
|
|
|
|
--log('after')
|
|
--log(serpent.block(global))
|
|
|
|
-- unload mods which failed to run
|
|
for i = 1, #failed_mods do
|
|
unload_mod(failed_mods[i])
|
|
end
|
|
|
|
debug_log({'hotpatch-info.complete', {'hotpatch-info.loading-installed-mods'}})
|
|
debug_log({'hotpatch-info.complete', {'hotpatch-info.on-load'}})
|
|
end
|
|
|
|
on_configuration_changed = function(config)
|
|
debug_log({'hotpatch-info.on-configuration-changed'})
|
|
local failed_mods = {}
|
|
for i = 1, #loaded_mods do
|
|
if not mod_on_configuration_changed(i, config) then
|
|
table.insert(failed_mods, i)
|
|
end
|
|
end
|
|
|
|
for i = 1, #failed_mods do
|
|
unload_mod(failed_mods[i])
|
|
end
|
|
debug_log({'hotpatch-info.complete', {'hotpatch-info.on-configuration-changed'}})
|
|
end
|
|
|
|
if debug_log_on_tick then
|
|
on_tick = function(e)
|
|
debug_log({'hotpatch-trace.event-processing', 'on_tick'})
|
|
local mod
|
|
local f
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
f = mod.on_tick
|
|
if f then
|
|
debug_log({'hotpatch-trace.event-running', 'on_tick'}, mod.name)
|
|
f(e)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
on_tick = function(e)
|
|
local mod
|
|
local f
|
|
for i = 1, #loaded_mods do
|
|
mod = loaded_mods[i]
|
|
f = mod.on_tick
|
|
if f then
|
|
f(e)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
script.on_init(on_init)
|
|
script.on_load(on_load)
|
|
script.on_configuration_changed(on_configuration_changed)
|
|
script.on_event(defines.events.on_tick, on_tick)
|
|
|
|
--private API, don't use this
|
|
local mod_tools_internal = setmetatable({
|
|
-- mod installation/uninstallation support functions
|
|
-- These take a mod NAME as a first argument
|
|
install_mod = install_mod,
|
|
find_installed_mod = find_installed_mod,
|
|
install_mod_file = install_mod_file,
|
|
uninstall_mod = uninstall_mod,
|
|
|
|
-- mod interaction functions
|
|
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
|
load_mod = load_mod,
|
|
find_loaded_mod = find_loaded_mod,
|
|
run_mod = run_mod,
|
|
reset_mod = reset_mod,
|
|
reset_mod_events = reset_mod_events,
|
|
register_mod_events = register_mod_events,
|
|
unload_mod = unload_mod,
|
|
|
|
-- internal callbacks when a mod registers events
|
|
register_event = register_event,
|
|
register_nth_tick = register_nth_tick,
|
|
register_on_tick = register_on_tick,
|
|
|
|
-- mod bootstrap functions
|
|
-- These take a LOADED INDEX as a first argument
|
|
mod_on_init = mod_on_init,
|
|
mod_on_load = mod_on_load,
|
|
mod_on_configuration_changed = mod_on_configuration_changed,
|
|
|
|
static_mods = static_mods,
|
|
console = console,
|
|
debug_log = debug_log,
|
|
loaded_mods = loaded_mods,
|
|
loaded_libraries = loaded_libraries,
|
|
installed_mods = setmetatable({}, {
|
|
__index = function(_, k)
|
|
return global.mods[k]
|
|
end,
|
|
__newindex = function(_, __, ___)
|
|
-- do nothing
|
|
end,
|
|
__len = function(_)
|
|
return #global.mods
|
|
end,
|
|
__pairs = function(t) local function iter(_, k) local v; v = global.mods[k+1]; if v then return k+1, v end; end; return iter, t, 0 end,
|
|
__ipairs = function(t) local function iter(_, k) local v; v = global.mods[k+1]; if v then return k+1, v end; end; return iter, t, 0 end,
|
|
__metatable = false,
|
|
})
|
|
},{
|
|
__index = function(_, k)
|
|
debug_log({'hotpatch-error.invalid-API-access', k}, nil, 3)
|
|
end,
|
|
__newindex = function(_, __, ___)
|
|
-- do nothing
|
|
end,
|
|
-- Don't let mods muck around
|
|
__metatable = false,
|
|
})
|
|
|
|
--public API
|
|
local mod_tools = setmetatable({
|
|
-- most code should use static_mod
|
|
static_mod = static_mod, -- (name, version, code, files)
|
|
set_debug_level = function(level)
|
|
--debug_level = tonumber(_ENV.debug_settings.level) or debug_levels[_ENV.debug_settings.level]
|
|
debug_level = tonumber(level) or debug_levels[level]
|
|
end,
|
|
},{
|
|
__index = mod_tools_internal,
|
|
__newindex = function(_, __, ___)
|
|
-- do nothing, read-only table
|
|
end,
|
|
-- Don't let mods muck around
|
|
__metatable = false,
|
|
})
|
|
|
|
|
|
return mod_tools |