The initial implementation of DML ended up loading mods quite late, which did give it the benefit of all `Manager`s being available. This change therefore moves mod loading until after those are initialized. But contrary to old DML, we still create a separate game state to make sure the game doesn't advance until mods are loaded. This avoids race conditions like the one where LogMeIn needs to come early in the load order.
209 lines
7.1 KiB
Lua
209 lines
7.1 KiB
Lua
local _G = _G
|
|
local rawget = rawget
|
|
local rawset = rawset
|
|
|
|
local log = function(category, format, ...)
|
|
local Log = rawget(_G, "Log")
|
|
if Log then
|
|
Log.info(category, format, ...)
|
|
else
|
|
print(string.format("[%s] %s", category or "", string.format(format or "", ...)))
|
|
end
|
|
end
|
|
|
|
log("mod_main", "Initializing mods...")
|
|
|
|
local require_store = {}
|
|
|
|
-- This token is treated as a string template and filled by DTMM during deployment.
|
|
-- This allows hiding unsafe I/O functions behind a setting.
|
|
-- It's also a valid table definition, thereby degrading gracefully when not replaced.
|
|
local is_io_enabled = {{ is_io_enabled }} -- luacheck: ignore 113
|
|
local lua_libs = {
|
|
debug = debug,
|
|
os = {
|
|
date = os.date,
|
|
time = os.time,
|
|
clock = os.clock,
|
|
getenv = os.getenv,
|
|
difftime = os.difftime,
|
|
},
|
|
load = load,
|
|
loadfile = loadfile,
|
|
loadstring = loadstring,
|
|
}
|
|
|
|
if is_io_enabled then
|
|
lua_libs.io = io
|
|
lua_libs.os = os
|
|
lua_libs.ffi = require("ffi")
|
|
end
|
|
|
|
Mods = {
|
|
-- Keep a backup of certain system libraries before
|
|
-- Fatshark's code scrubs them.
|
|
-- The loader can then decide to pass them on to mods, or ignore them
|
|
lua = setmetatable({}, { __index = lua_libs }),
|
|
require_store = require_store,
|
|
original_require = require,
|
|
}
|
|
|
|
local can_insert = function(filepath, new_result)
|
|
local store = require_store[filepath]
|
|
if not store or #store then
|
|
return true
|
|
end
|
|
|
|
if store[#store] ~= new_result then
|
|
return true
|
|
end
|
|
end
|
|
|
|
local original_require = require
|
|
require = function(filepath, ...)
|
|
local result = original_require(filepath, ...)
|
|
if result and type(result) == "table" then
|
|
if can_insert(filepath, result) then
|
|
require_store[filepath] = require_store[filepath] or {}
|
|
local store = require_store[filepath]
|
|
|
|
table.insert(store, result)
|
|
|
|
if Mods.hook then
|
|
Mods.hook.enable_by_file(filepath, #store)
|
|
end
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
require("scripts/boot_init")
|
|
require("scripts/foundation/utilities/class")
|
|
|
|
-- The `__index` metamethod maps a proper identifier `CLASS.MyClassName` to the
|
|
-- stringified version of the key: `"MyClassName"`.
|
|
-- This allows using LuaCheck for the stringified class names in hook parameters.
|
|
_G.CLASS = setmetatable({}, {
|
|
__index = function(_, key)
|
|
return key
|
|
end
|
|
})
|
|
|
|
local original_class = class
|
|
class = function(class_name, super_name, ...)
|
|
local result = original_class(class_name, super_name, ...)
|
|
if not rawget(_G, class_name) then
|
|
rawset(_G, class_name, result)
|
|
end
|
|
if not rawget(_G.CLASS, class_name) then
|
|
rawset(_G.CLASS, class_name, result)
|
|
end
|
|
return result
|
|
end
|
|
|
|
require("scripts/main")
|
|
log("mod_main", "'scripts/main' loaded")
|
|
|
|
-- Inject our state into the game. The state needs to run after `StateGame._init_managers`,
|
|
-- since some parts of DMF, and presumably other mods, depend on some of those managers to exist.
|
|
local function patch_mod_loading_state()
|
|
local StateBootSubStateBase = require("scripts/game_states/boot/state_boot_sub_state_base")
|
|
local StateBootLoadDML = class("StateBootLoadDML", "StateBootSubStateBase")
|
|
local StateGameLoadMods = class("StateGameLoadMods")
|
|
|
|
StateBootLoadDML.on_enter = function(self, parent, params)
|
|
log("StateBootLoadDML", "Entered")
|
|
StateBootLoadDML.super.on_enter(self, parent, params)
|
|
|
|
local state_params = self:_state_params()
|
|
local package_manager = state_params.package_manager
|
|
|
|
self._package_manager = package_manager
|
|
self._package_handles = {
|
|
["packages/mods"] = package_manager:load("packages/mods", "StateBootDML", nil),
|
|
["packages/dml"] = package_manager:load("packages/dml", "StateBootDML", nil),
|
|
}
|
|
end
|
|
|
|
StateBootLoadDML._state_update = function(self, dt)
|
|
local package_manager = self._package_manager
|
|
|
|
if package_manager:update() then
|
|
local DML = require("scripts/mods/dml/init")
|
|
local mod_data = require("scripts/mods/mod_data")
|
|
local mod_loader = DML.create_loader(mod_data)
|
|
Managers.mod = mod_loader
|
|
log("StateBootLoadDML", "DML loaded, exiting")
|
|
return true, false
|
|
end
|
|
|
|
return false, false
|
|
end
|
|
|
|
|
|
function StateGameLoadMods:on_enter(_, params)
|
|
log("StateGameLoadMods", "Entered")
|
|
self._next_state = require("scripts/game_states/game/state_splash")
|
|
self._next_state_params = params
|
|
end
|
|
|
|
function StateGameLoadMods:update(main_dt)
|
|
local state = self._loading_state
|
|
|
|
-- We're relying on the fact that DML internally makes sure
|
|
-- that `Managers.mod:update()` is being called appropriately.
|
|
-- The implementation as of this writing is to hook `StateGame.update`.
|
|
if Managers.mod:all_mods_loaded() then
|
|
Log.info("StateGameLoadMods", "Mods loaded, exiting")
|
|
return self._next_state, self._next_state_params
|
|
end
|
|
end
|
|
|
|
local GameStateMachine = require("scripts/foundation/utilities/game_state_machine")
|
|
local GameStateMachine_init = GameStateMachine.init
|
|
GameStateMachine.init = function(self, parent, start_state, params, creation_context, state_change_callbacks, name)
|
|
if name == "Main" then
|
|
log("mod_main", "Injecting StateBootLoadDML")
|
|
|
|
-- Hardcoded position after `StateRequireScripts`.
|
|
-- We need to wait until then to even begin most of our stuff,
|
|
-- so that most of the game's core systems are at least loaded and can be hooked,
|
|
-- even if they aren't running, yet.
|
|
local pos = 4
|
|
table.insert(params.states, pos, {
|
|
StateBootLoadDML,
|
|
{
|
|
package_manager = params.package_manager,
|
|
},
|
|
})
|
|
|
|
GameStateMachine_init(self, parent, start_state, params, creation_context, state_change_callbacks, name)
|
|
elseif name == "Game" then
|
|
log("mod_main", "Injection StateGameLoadMods")
|
|
-- The second time around, we want to be the first, so we pass our own
|
|
-- 'start_state'.
|
|
-- We can't just have the state machine be initialized and then change its `_next_state`, as by the end of
|
|
-- `init`, a bunch of stuff will already be initialized.
|
|
GameStateMachine_init(self, parent, StateGameLoadMods, params, creation_context, state_change_callbacks, name)
|
|
-- And since we're done now, we can revert the function to its original
|
|
GameStateMachine.init = GameStateMachine_init
|
|
|
|
return
|
|
else
|
|
-- In all other cases, simply call the original
|
|
GameStateMachine_init(self, parent, start_state, params, creation_context, state_change_callbacks, name)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Override `init` to run our injection
|
|
function init()
|
|
patch_mod_loading_state()
|
|
|
|
-- As requested by Fatshark
|
|
local StateRequireScripts = require("scripts/game_states/boot/state_require_scripts")
|
|
StateRequireScripts._get_is_modded = function() return true end
|
|
|
|
Main:init()
|
|
end
|