Use template engine to build mod_data.lua

The string-building version became too complex to maintain properly.
This commit is contained in:
Lucas Schwiderski 2023-11-12 23:20:10 +01:00
parent b1ff69fa08
commit d0e074ccce
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
5 changed files with 99 additions and 68 deletions

10
Cargo.lock generated
View file

@ -906,6 +906,7 @@ dependencies = [
"futures",
"lazy_static",
"luajit2-sys",
"minijinja",
"nexusmods",
"oodle",
"path-slash",
@ -2084,6 +2085,15 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minijinja"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "208758577ef2c86cf5dd3e85730d161413ec3284e2d73b2ef65d9a24d9971bcb"
dependencies = [
"serde",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"

View file

@ -35,3 +35,4 @@ ansi-parser = "0.9.0"
string_template = "0.2.1"
luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" }
async-recursion = "1.0.5"
minijinja = "1.0.10"

View file

@ -0,0 +1,27 @@
return {
{% for mod in mods %}
{
id = "{{ mod.id }}",
name = "{{ mod.name }}",
bundled = {{ mod.bundled }},
packages = {
{% for pkg in mod.packages %}
"{{ pkg }}",
{% endfor %}
},
run = function()
{% if mod.data is none %}
return dofile("{{ mod.init }}")
{% else %}
new_mod("{{ mod.id }}", {
mod_script = "{{ mod.init }}",
mod_data = "{{ mod.data }}",
{% if not mod.localization is none %}
mod_localization = "{{ mod.localization }}",
{% endif %}
})
{% endif %}
end,
},
{% endfor %}
}

View file

@ -61,14 +61,14 @@ local function patch_mod_loading_state()
if state == "load_package" and package_manager:update() then
log("StateBootLoadMods", "Packages loaded, loading mods")
self._state = "load_mods"
local ModLoader = require("scripts/mods/dml/init")
local DML = require("scripts/mods/dml/init")
local mod_data = require("scripts/mods/mod_data")
local mod_loader = ModLoader:new(mod_data, self._parent:gui())
local mod_loader = DML.create_loader(mod_data, self._parent:gui())
self._mod_loader = mod_loader
self._dml = DML
Managers.mod = mod_loader
elseif state == "load_mods" and self._mod_loader:update(dt) then
elseif state == "load_mods" and self._dml.update(Managers.mod, dt) then
log("StateBootLoadMods", "Mods loaded, exiting")
return true, false
end
@ -112,7 +112,7 @@ 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 is_io_enabled = {{ is_io_enabled }} -- luacheck: ignore 113
local lua_libs = {
debug = debug,
os = {

View file

@ -8,7 +8,7 @@ use color_eyre::eyre::Context;
use color_eyre::{eyre, Help, Report, Result};
use futures::StreamExt;
use futures::{stream, TryStreamExt};
use path_slash::PathBufExt;
use minijinja::Environment;
use sdk::filetype::lua;
use sdk::filetype::package::Package;
use sdk::murmur::Murmur64;
@ -201,10 +201,10 @@ async fn copy_recursive(
async move {
if is_dir {
tracing::trace!("Creating directory '{}'", dest.display());
fs::create_dir(&dest)
.await
.map(|_| ())
.wrap_err_with(|| format!("Failed to create directory '{}'", dest.display()))
// Instead of trying to filter "already exists" errors out explicitly,
// we just ignore all. It'll fail eventually with the next copy operation.
let _ = fs::create_dir(&dest).await;
Ok(())
} else {
tracing::trace!("Copying file '{}' -> '{}'", path.display(), dest.display());
fs::copy(&path, &dest).await.map(|_| ()).wrap_err_with(|| {
@ -262,67 +262,60 @@ async fn copy_mod_folders(state: Arc<ActionState>) -> Result<Vec<String>> {
Ok(ids)
}
fn build_mod_data_lua(state: Arc<ActionState>) -> String {
let mut lua = String::from("return {\n");
// DMF is handled explicitely by the loading procedures, as it actually drives most of that
// and should therefore not show up in the load order.
for mod_info in state.mods.iter().filter(|m| m.id != "dml" && m.enabled) {
lua.push_str(" {\n name = \"");
lua.push_str(&mod_info.name);
lua.push_str("\",\n id = \"");
lua.push_str(&mod_info.id);
lua.push_str("\",\n bundled = \"");
if mod_info.bundled {
lua.push_str("true");
} else {
lua.push_str("false");
fn build_mod_data_lua(state: Arc<ActionState>) -> Result<String> {
#[derive(Serialize)]
struct TemplateDataMod {
id: String,
name: String,
bundled: bool,
init: String,
data: Option<String>,
localization: Option<String>,
packages: Vec<String>,
}
lua.push_str("\",\n run = function()\n");
let mut env = Environment::new();
env.add_template("mod_data.lua", include_str!("../../assets/mod_data.lua.j2"))
.wrap_err("Failed to compile template for `mod_data.lua`")?;
let tmpl = env
.get_template("mod_data.lua")
.wrap_err("Failed to get template `mod_data.lua`")?;
let resources = &mod_info.resources;
if resources.data.is_some() || resources.localization.is_some() {
lua.push_str(" new_mod(\"");
lua.push_str(&mod_info.id);
lua.push_str("\", {\n mod_script = \"");
lua.push_str(&resources.init.to_slash_lossy());
if let Some(data) = resources.data.as_ref() {
lua.push_str("\",\n mod_data = \"");
lua.push_str(&data.to_slash_lossy());
let data: Vec<TemplateDataMod> = state
.mods
.iter()
.filter_map(|m| {
if m.id == "dml" || !m.enabled {
return None;
}
if let Some(localization) = &resources.localization {
lua.push_str("\",\n mod_localization = \"");
lua.push_str(&localization.to_slash_lossy());
}
Some(TemplateDataMod {
id: m.id.clone(),
name: m.name.clone(),
bundled: m.bundled,
init: m.resources.init.to_string_lossy().to_string(),
data: m
.resources
.data
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
localization: m
.resources
.localization
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
packages: m.packages.iter().map(|p| p.name.clone()).collect(),
})
})
.collect();
lua.push_str("\",\n })\n");
} else {
lua.push_str(" return dofile(\"");
lua.push_str(&resources.init.to_slash_lossy());
lua.push_str("\")\n");
}
let lua = tmpl
.render(minijinja::context!(mods => data))
.wrap_err("Failed to render template `mod_data.lua`")?;
lua.push_str(" end,\n packages = {\n");
tracing::debug!("mod_data.lua:\n{}", lua);
for pkg_info in &mod_info.packages {
lua.push_str(" \"");
lua.push_str(&pkg_info.name);
lua.push_str("\",\n");
}
lua.push_str(" },\n },\n");
}
lua.push('}');
tracing::debug!("mod_data_lua:\n{}", lua);
lua
Ok(lua)
}
#[tracing::instrument(skip_all)]
@ -340,7 +333,7 @@ async fn build_bundles(state: Arc<ActionState>) -> Result<Vec<Bundle>> {
let span = tracing::debug_span!("Building mod data script");
let _enter = span.enter();
let lua = build_mod_data_lua(state.clone());
let lua = build_mod_data_lua(state.clone()).wrap_err("Failed to build Lua mod data")?;
tracing::trace!("Compiling mod data script");