All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
215 lines
7.3 KiB
Django/Jinja
215 lines
7.3 KiB
Django/Jinja
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.
|
|
-- When not replaced, it's also a valid table definition, thereby degrading gracefully.
|
|
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")
|
|
|
|
-- We need to inject two states into two different state machines:
|
|
-- First, we inject one into the `"Main"` state machine at a specific location, so that we're
|
|
-- still early in the process, but right after `StateRequireScripts` where most game files
|
|
-- are already available to `require` and hook.
|
|
-- This is where the `ModLoader` is created initially.
|
|
-- Then, we inject into the very first position of the `"Game"` state machine. This runs right
|
|
-- after `StateGame._init_managers`, at which point all the parts needed for DMF and other mods
|
|
-- have been initialized.
|
|
-- This is where `ModLoader` will finally start loading mods.
|
|
local function patch_mod_loading_state()
|
|
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", "StateBootLoadDML", nil),
|
|
}
|
|
end
|
|
|
|
StateBootLoadDML._state_update = function(self, _)
|
|
local package_manager = self._package_manager
|
|
|
|
if package_manager:update() then
|
|
local mod_data = require("scripts/mods/mod_data")
|
|
|
|
local create_mod_loader = require("scripts/mods/init")
|
|
local mod_loader = create_mod_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(_)
|
|
-- 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
|
|
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
|
|
|
|
-- vim: ft=lua
|