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...") log("mod_main", "[DTMM] Deployment data:\n{{ deployment_info }}") 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