Files
2019-03-13 20:13:03 +01:00

1339 lines
45 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 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
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
-- 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}}
-- 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
-- mapping of events to names for logging
local event_names = {}
for k,v in pairs(defines.events) do
event_names[v] = k
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
-- TODO: notify about installing over top of existing mod
mod = global.mods[index]
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
--local mod_global = global.mod_global[mod_name]
--for k, v in pairs(mod_global) do
-- mod_global[k] = nil
--end
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
run_mod = function(loaded_index)
local mod = loaded_mods[loaded_index]
if mod then
local mod_name = mod.name
local old_global = table.deepcopy(mod.env.global)
if strict_mode then
--mod.env.global = {}
end
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
if strict_mode then
--if mod.env.global ~= {} then
--TODO: error, mod touched global inappropriately during load
--end
--mod.env.global = old_global
end
-- if a mod is being loaded that already has global data, it must be restored after
-- running control.lua, discarding any changes made
if not table.compare(old_global, {}) then
for k,_ in pairs(mod.env.global) do
mod.env.global[k] = nil
end
for k,v in pairs(old_global) do
mod.env.global[k] = v
end
--mod.env.global = old_global
end
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 = table.deepcopy(mod.env.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
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 = {}
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
-- 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