wip: Add preliminary mod loading injection
This commit is contained in:
parent
0cf2908904
commit
571ae9605a
3 changed files with 507 additions and 15 deletions
71
crates/dtmm/assets/mod_main.lua
Normal file
71
crates/dtmm/assets/mod_main.lua
Normal file
|
@ -0,0 +1,71 @@
|
|||
Mods = {
|
||||
-- Keep a backup of certain system libraries before
|
||||
-- Fatshark's code scrubs them.
|
||||
-- The metatable setup prevents mods from overwriting them.
|
||||
lua = setmetatable({}, {
|
||||
__index = { io = io, debug = debug, ffi = ffi, os = os },
|
||||
}),
|
||||
}
|
||||
|
||||
require("scripts/game_states/boot/state_boot_sub_state_base")
|
||||
local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase")
|
||||
|
||||
StateBootLoadMods.on_enter = function (self, parent, params)
|
||||
StateBootLoadMods.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", "StateBootLoadMods", nil),
|
||||
["packages/dmf"] = package_manager:load("packages/dmf", "StateBootLoadMods", nil),
|
||||
}
|
||||
end
|
||||
|
||||
StateBootLoadMods._state_update = function (self, dt)
|
||||
local state = self._state
|
||||
local package_manager = self._package_manager
|
||||
|
||||
if state == "load_package" and package_manager:update() then
|
||||
self._state = "load_mods"
|
||||
local dmf_loader = require("scripts/mods/dmf/dmf_loader")
|
||||
self._dmf_loader = dmf_loader
|
||||
|
||||
local mod_data = require("scripts/mods/mod_data")
|
||||
dmf_loader:init(self._parent.gui, mod_data)
|
||||
elseif state == "load_mods" and self._dmf_loader:update(dt) then
|
||||
return true, false
|
||||
end
|
||||
|
||||
return false, false
|
||||
end
|
||||
|
||||
require("scripts/main")
|
||||
|
||||
-- Patch `GameStateMachine.init` to add our own state for loading mods.
|
||||
-- In the future, Fatshark might provide us with a dedicated way to do this.
|
||||
local function patch_mod_loading_state()
|
||||
local GameStateMachine = require("scripts/foundations/utilities/game_state_machine")
|
||||
|
||||
local GameStateMachine_init = GameStateMachine.init
|
||||
GameStateMachine.init = function(self, parent, start_state, params, ...)
|
||||
-- Hardcoded position after `StateRequireScripts`.
|
||||
-- We do want to wait until then, 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.state, pos, {
|
||||
StateBootLoadMods,
|
||||
{
|
||||
package_manager = params.package_manager,
|
||||
},
|
||||
})
|
||||
|
||||
return GameStateMachine_init(self, parent, start_state, params, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function init()
|
||||
Main.init()
|
||||
patch_mod_loading_state()
|
||||
end
|
357
crates/dtmm/assets/mod_manager.lua
Normal file
357
crates/dtmm/assets/mod_manager.lua
Normal file
|
@ -0,0 +1,357 @@
|
|||
-- Copyright on this file is owned by Fatshark.
|
||||
-- It is extracted, used and modified with permission only for
|
||||
-- the purpose of loading mods within Warhammer 40,000: Darktide.
|
||||
local ModManager = class("ModManager")
|
||||
|
||||
local MOD_DATA = require("scripts/mods/mod_data")
|
||||
|
||||
local LOG_LEVELS = {
|
||||
spew = 4,
|
||||
info = 3,
|
||||
warning = 2,
|
||||
error = 1
|
||||
}
|
||||
local DEFAULT_SETTINGS = {
|
||||
log_level = LOG_LEVELS.error,
|
||||
developer_mode = false
|
||||
}
|
||||
|
||||
local Keyboard = Keyboard
|
||||
local BUTTON_INDEX_R = Keyboard.button_index("r")
|
||||
local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift")
|
||||
local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl")
|
||||
|
||||
ModManager.init = function(self, boot_gui)
|
||||
self._mods = {}
|
||||
self._num_mods = nil
|
||||
self._state = "not_loaded"
|
||||
self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS
|
||||
|
||||
self._chat_print_buffer = {}
|
||||
self._reload_data = {}
|
||||
self._gui = boot_gui
|
||||
self._ui_time = 0
|
||||
self._network_callbacks = {}
|
||||
|
||||
Crashify.print_property("realm", "modded")
|
||||
|
||||
self._state = "scanning"
|
||||
end
|
||||
|
||||
ModManager.developer_mode_enabled = function(self)
|
||||
return self._settings.developer_mode
|
||||
end
|
||||
|
||||
ModManager._draw_state_to_gui = function(self, gui, dt)
|
||||
local state = self._state
|
||||
local t = self._ui_time + dt
|
||||
self._ui_time = t
|
||||
local status_str = "Loading mods"
|
||||
|
||||
if state == "scanning" then
|
||||
status_str = "Scanning for mods"
|
||||
elseif state == "loading" then
|
||||
local mod = self._mods[self._mod_load_index]
|
||||
status_str = string.format("Loading mod %q", mod.name)
|
||||
end
|
||||
|
||||
Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1))
|
||||
end
|
||||
|
||||
ModManager.remove_gui = function(self)
|
||||
self._gui = nil
|
||||
end
|
||||
|
||||
ModManager._has_enabled_mods = function()
|
||||
return true
|
||||
end
|
||||
|
||||
ModManager._check_reload = function()
|
||||
return Keyboard.pressed(BUTTON_INDEX_R) and
|
||||
Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) +
|
||||
Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2
|
||||
end
|
||||
|
||||
ModManager.update = function(self, dt)
|
||||
local chat_print_buffer = self._chat_print_buffer
|
||||
local num_delayed_prints = #chat_print_buffer
|
||||
|
||||
if num_delayed_prints > 0 and Managers.chat then
|
||||
for i = 1, num_delayed_prints, 1 do
|
||||
-- TODO: Use new chat system
|
||||
-- Managers.chat:add_local_system_message(1, chat_print_buffer[i], true)
|
||||
|
||||
chat_print_buffer[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local old_state = self._state
|
||||
|
||||
if self._settings.developer_mode and self:_check_reload() then
|
||||
self._reload_requested = true
|
||||
end
|
||||
|
||||
if self._reload_requested and self._state == "done" then
|
||||
self:_reload_mods()
|
||||
end
|
||||
|
||||
if self._state == "done" then
|
||||
for i = 1, self._num_mods, 1 do
|
||||
local mod = self._mods[i]
|
||||
|
||||
if mod and not mod.callbacks_disabled then
|
||||
self:_run_callback(mod, "update", dt)
|
||||
end
|
||||
end
|
||||
elseif self._state == "scanning" then
|
||||
self:_build_mod_table()
|
||||
|
||||
self._state = self:_load_mod(1)
|
||||
self._ui_time = 0
|
||||
elseif self._state == "loading" then
|
||||
local handle = self._loading_resource_handle
|
||||
|
||||
if ResourcePackage.has_loaded(handle) then
|
||||
ResourcePackage.flush(handle)
|
||||
|
||||
local mod = self._mods[self._mod_load_index]
|
||||
local next_index = mod.package_index + 1
|
||||
local mod_data = mod.data
|
||||
|
||||
if next_index > #mod_data.packages then
|
||||
mod.state = "running"
|
||||
local ok, object = pcall(mod_data.run)
|
||||
|
||||
if not ok then self:print("error", "%s", object) end
|
||||
|
||||
local name = mod.name
|
||||
mod.object = object or {}
|
||||
|
||||
self:_run_callback(mod, "init", self._reload_data[mod.id])
|
||||
self:print("info", "%s loaded.", name)
|
||||
|
||||
self._state = self:_load_mod(self._mod_load_index + 1)
|
||||
else
|
||||
self:_load_package(mod, next_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local gui = self._gui
|
||||
|
||||
if gui then self:_draw_state_to_gui(gui, dt) end
|
||||
|
||||
if old_state ~= self._state then
|
||||
self:print("info", "%s -> %s", old_state, self._state)
|
||||
end
|
||||
end
|
||||
|
||||
ModManager.all_mods_loaded = function(self)
|
||||
return self._state == "done"
|
||||
end
|
||||
|
||||
ModManager.destroy = function(self)
|
||||
self:unload_all_mods()
|
||||
end
|
||||
|
||||
ModManager._run_callback = function(self, mod, callback_name, ...)
|
||||
local object = mod.object
|
||||
local cb = object[callback_name]
|
||||
|
||||
if not cb then
|
||||
return
|
||||
end
|
||||
|
||||
local success, val = pcall(cb, object, ...)
|
||||
|
||||
if success then
|
||||
return val
|
||||
else
|
||||
self:print("error", "%s", val or "[unknown error]")
|
||||
self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.",
|
||||
callback_name, mod.name, mod.id)
|
||||
|
||||
mod.callbacks_disabled = true
|
||||
end
|
||||
end
|
||||
|
||||
ModManager._start_scan = function(self)
|
||||
self:print("info", "Starting mod scan")
|
||||
self._state = "scanning"
|
||||
end
|
||||
|
||||
ModManager._build_mod_table = function(self)
|
||||
fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table")
|
||||
|
||||
for i, mod_data in ipairs(MOD_DATA) do
|
||||
printf("[ModManager] mods[%d] = name=%q", i, mod_data.name)
|
||||
|
||||
self._mods[i] = {
|
||||
id = i,
|
||||
state = "not_loaded",
|
||||
callbacks_disabled = false,
|
||||
name = mod_data.name,
|
||||
loaded_packages = {},
|
||||
packages = mod_data.packages,
|
||||
}
|
||||
end
|
||||
|
||||
self._num_mods = #self._mods
|
||||
|
||||
self:print("info", "Found %i mods", #self._mods)
|
||||
end
|
||||
|
||||
ModManager._load_mod = function(self, index)
|
||||
self._ui_time = 0
|
||||
local mods = self._mods
|
||||
local mod = mods[index]
|
||||
|
||||
if not mod then
|
||||
table.clear(self._reload_data)
|
||||
|
||||
return "done"
|
||||
end
|
||||
|
||||
self:print("info", "loading mod %i", mod.id)
|
||||
Crashify.print_property("modded", true)
|
||||
|
||||
mod.state = "loading"
|
||||
|
||||
Crashify.print_property(string.format("Mod:%i:%s", mod.id, mod.name), true)
|
||||
|
||||
self._mod_load_index = index
|
||||
|
||||
self:_load_package(mod, 1)
|
||||
|
||||
return "loading"
|
||||
end
|
||||
|
||||
ModManager._load_package = function(self, mod, index)
|
||||
mod.package_index = index
|
||||
local package_name = mod.packages[index]
|
||||
|
||||
if not package_name then
|
||||
return
|
||||
end
|
||||
|
||||
self:print("info", "loading package %q", package_name)
|
||||
|
||||
local resource_handle = Application.resource_package(package_name)
|
||||
self._loading_resource_handle = resource_handle
|
||||
|
||||
ResourcePackage.load(resource_handle)
|
||||
|
||||
mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle
|
||||
end
|
||||
|
||||
ModManager.unload_all_mods = function(self)
|
||||
if self._state ~= "done" then
|
||||
self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
self:print("info", "Unload all mod packages")
|
||||
|
||||
for i = self._num_mods, 1, -1 do
|
||||
local mod = self._mods[i]
|
||||
|
||||
if mod then
|
||||
self:unload_mod(i)
|
||||
end
|
||||
|
||||
self._mods[i] = nil
|
||||
end
|
||||
|
||||
self._num_mods = nil
|
||||
self._state = "unloaded"
|
||||
end
|
||||
|
||||
ModManager.unload_mod = function(self, index)
|
||||
local mod = self._mods[index]
|
||||
|
||||
if mod then
|
||||
self:print("info", "Unloading %q.", mod.name)
|
||||
self:_run_callback(mod, "on_unload")
|
||||
|
||||
for _, handle in ipairs(mod.loaded_packages) do
|
||||
ResourcePackage.unload(handle)
|
||||
Application.release_resource_package(handle)
|
||||
end
|
||||
|
||||
mod.state = "not_loaded"
|
||||
else
|
||||
self:print("error", "Mod index %i can't be unloaded, has not been loaded", index)
|
||||
end
|
||||
end
|
||||
|
||||
ModManager._reload_mods = function(self)
|
||||
self:print("info", "reloading mods")
|
||||
|
||||
for i = 1, self._num_mods, 1 do
|
||||
local mod = self._mods[i]
|
||||
|
||||
if mod and mod.state == "running" then
|
||||
self:print("info", "reloading %s", mod.name)
|
||||
|
||||
self._reload_data[i] = self:_run_callback(mod, "on_reload")
|
||||
else
|
||||
self:print("info", "not reloading mod, state: %s", mod.state)
|
||||
end
|
||||
end
|
||||
|
||||
self:unload_all_mods()
|
||||
self:_start_scan()
|
||||
|
||||
self._reload_requested = false
|
||||
end
|
||||
|
||||
ModManager.on_game_state_changed = function(self, status, state_name, state_object)
|
||||
if self._state == "done" then
|
||||
for i = 1, self._num_mods, 1 do
|
||||
local mod = self._mods[i]
|
||||
|
||||
if mod and not mod.callbacks_disabled then
|
||||
self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object)
|
||||
end
|
||||
end
|
||||
else
|
||||
self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state)
|
||||
end
|
||||
end
|
||||
|
||||
ModManager.print = function(self, level, str, ...)
|
||||
local message = string.format("[ModManager][" .. level .. "] " .. str, ...)
|
||||
local log_level = LOG_LEVELS[level] or 99
|
||||
|
||||
if log_level <= 2 then
|
||||
print(message)
|
||||
end
|
||||
|
||||
if log_level <= self._settings.log_level then
|
||||
self._chat_print_buffer[#self._chat_print_buffer + 1] = message
|
||||
end
|
||||
end
|
||||
|
||||
local function noop()
|
||||
end
|
||||
|
||||
ModManager.network_bind = noop
|
||||
|
||||
ModManager.network_unbind = noop
|
||||
|
||||
ModManager.network_is_occupied = function()
|
||||
return false
|
||||
end
|
||||
|
||||
ModManager.network_send = noop
|
||||
|
||||
ModManager.rpc_mod_user_data = noop
|
||||
|
||||
ModManager.register_network_event_delegate = noop
|
||||
|
||||
ModManager.unregister_network_event_delegate = noop
|
||||
|
||||
ModManager.network_context_created = noop
|
||||
|
||||
return ModManager
|
|
@ -6,7 +6,7 @@ use std::str::FromStr;
|
|||
use std::sync::Arc;
|
||||
|
||||
use color_eyre::eyre::Context;
|
||||
use color_eyre::{eyre, Result};
|
||||
use color_eyre::{eyre, Help, Result};
|
||||
use druid::FileInfo;
|
||||
use futures::stream;
|
||||
use futures::StreamExt;
|
||||
|
@ -28,6 +28,7 @@ const MOD_BUNDLE_NAME: &str = "packages/mods";
|
|||
const BOOT_BUNDLE_NAME: &str = "packages/boot";
|
||||
const BUNDLE_DATABASE_NAME: &str = "bundle_database.data";
|
||||
const MOD_BOOT_SCRIPT: &str = "scripts/mod_main";
|
||||
const MOD_DATA_SCRIPT: &str = "scripts/mods/mod_data";
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn read_file_with_backup<P>(path: P) -> Result<Vec<u8>>
|
||||
|
@ -138,6 +139,52 @@ fn make_package(info: &PackageInfo) -> Result<Package> {
|
|||
Ok(pkg)
|
||||
}
|
||||
|
||||
fn build_mod_data_lua(state: Arc<State>) -> String {
|
||||
let mut lua = String::from("return {\n");
|
||||
|
||||
for mod_info in state.get_mods().iter().filter(|m| m.get_enabled()) {
|
||||
lua.push_str(" {\n name = \"");
|
||||
lua.push_str(mod_info.get_name());
|
||||
|
||||
lua.push_str("\",\n id = \"");
|
||||
lua.push_str(mod_info.get_id());
|
||||
|
||||
lua.push_str("\",\n run = function()\n");
|
||||
|
||||
if mod_info.get_name() == "dmf" {
|
||||
lua.push_str(" return dofile(\"");
|
||||
lua.push_str(mod_info.get_resources().get_init());
|
||||
lua.push_str("\")\n");
|
||||
} else {
|
||||
lua.push_str(" return new_mod(\"");
|
||||
lua.push_str(mod_info.get_name());
|
||||
lua.push_str("\", {\n init = \"");
|
||||
lua.push_str(mod_info.get_resources().get_init());
|
||||
lua.push_str("\",\n data = \"");
|
||||
lua.push_str(mod_info.get_resources().get_data());
|
||||
lua.push_str("\",\n localization = \"");
|
||||
lua.push_str(mod_info.get_resources().get_localization());
|
||||
lua.push_str("\",\n })\n");
|
||||
}
|
||||
|
||||
lua.push_str(" end,\n packages = [\n");
|
||||
|
||||
for pkg_info in mod_info.get_packages() {
|
||||
lua.push_str(" \"");
|
||||
lua.push_str(pkg_info.get_name());
|
||||
lua.push_str("\",\n");
|
||||
}
|
||||
|
||||
lua.push_str(" ]\n }\n");
|
||||
}
|
||||
|
||||
lua.push('}');
|
||||
|
||||
tracing::debug!("mod_data_lua:\n{}", lua);
|
||||
|
||||
lua
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn build_bundles(state: Arc<State>) -> Result<()> {
|
||||
let mut bundle = Bundle::new(MOD_BUNDLE_NAME.into());
|
||||
|
@ -156,6 +203,31 @@ async fn build_bundles(state: Arc<State>) -> Result<()> {
|
|||
db
|
||||
};
|
||||
|
||||
{
|
||||
let span = tracing::debug_span!("Building mod data script");
|
||||
let _enter = span.enter();
|
||||
|
||||
let lua = build_mod_data_lua(state.clone());
|
||||
let lua = CString::new(lua).wrap_err("failed to build CString from mod data Lua string")?;
|
||||
let file =
|
||||
lua::compile(MOD_DATA_SCRIPT, &lua).wrap_err("failed to compile mod data Lua file")?;
|
||||
|
||||
bundle.add_file(file);
|
||||
}
|
||||
|
||||
{
|
||||
let span = tracing::debug_span!("Importing mod manager script");
|
||||
let _enter = span.enter();
|
||||
|
||||
let lua = include_str!("../assets/mod_manager.lua");
|
||||
let lua =
|
||||
CString::new(lua).wrap_err("failed to build CString from mod manager Lua string")?;
|
||||
let file = lua::compile(MOD_MANAGER_SCRIPT, &lua)
|
||||
.wrap_err("failed to compile mod manager Lua file")?;
|
||||
|
||||
bundle.add_file(file);
|
||||
}
|
||||
|
||||
for mod_info in state.get_mods().iter().filter(|m| m.get_enabled()) {
|
||||
let span = tracing::trace_span!("building mod packages", name = mod_info.get_name());
|
||||
let _enter = span.enter();
|
||||
|
@ -296,23 +368,15 @@ async fn patch_boot_bundle(state: Arc<State>) -> Result<()> {
|
|||
}
|
||||
|
||||
{
|
||||
tracing::trace!("Adding main mod Lua file to boot bundle");
|
||||
let span = tracing::trace_span!("create mod boot script file");
|
||||
let span = tracing::debug_span!("Importing mod main script");
|
||||
let _enter = span.enter();
|
||||
|
||||
// TODO: Build actual boot script
|
||||
let lua = CString::new(
|
||||
r#"
|
||||
print("dtmm says hello!")
|
||||
require("scripts/main")
|
||||
"#,
|
||||
)
|
||||
.expect("invalid C string");
|
||||
let f = lua::compile(MOD_BOOT_SCRIPT.to_string(), &lua)
|
||||
.wrap_err("failed to compile mod boot script")?;
|
||||
let lua = include_str!("../assets/mod_main.lua");
|
||||
let lua = CString::new(lua).wrap_err("failed to build CString from mod main Lua string")?;
|
||||
let file =
|
||||
lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("failed to compile mod main Lua file")?;
|
||||
|
||||
// TODO:
|
||||
bundle.add_file(f);
|
||||
bundle.add_file(file);
|
||||
}
|
||||
|
||||
db.add_bundle(&bundle);
|
||||
|
|
Loading…
Add table
Reference in a new issue