From eee1f500b8a8b6bae9fc6737048baaec7a62d62e Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sun, 12 Nov 2023 23:20:10 +0100 Subject: [PATCH] Use template engine to build `mod_data.lua` The string-building version became too complex to maintain properly. --- Cargo.lock | 10 +++ crates/dtmm/Cargo.toml | 1 + crates/dtmm/assets/mod_data.lua.j2 | 27 ++++++ crates/dtmm/assets/mod_main.lua | 10 +-- crates/dtmm/src/controller/deploy.rs | 119 +++++++++++++-------------- 5 files changed, 99 insertions(+), 68 deletions(-) create mode 100644 crates/dtmm/assets/mod_data.lua.j2 diff --git a/Cargo.lock b/Cargo.lock index 011fa6b..8385e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 1842a62..5f60220 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -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" diff --git a/crates/dtmm/assets/mod_data.lua.j2 b/crates/dtmm/assets/mod_data.lua.j2 new file mode 100644 index 0000000..9f87ad1 --- /dev/null +++ b/crates/dtmm/assets/mod_data.lua.j2 @@ -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 %} +} diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 6dbf3e2..a17f5be 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -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 = { diff --git a/crates/dtmm/src/controller/deploy.rs b/crates/dtmm/src/controller/deploy.rs index ded840e..53c4ef1 100644 --- a/crates/dtmm/src/controller/deploy.rs +++ b/crates/dtmm/src/controller/deploy.rs @@ -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) -> Result> { Ok(ids) } -fn build_mod_data_lua(state: Arc) -> 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"); - } - - lua.push_str("\",\n run = function()\n"); - - 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()); - } - - if let Some(localization) = &resources.localization { - lua.push_str("\",\n mod_localization = \""); - lua.push_str(&localization.to_slash_lossy()); - } - - lua.push_str("\",\n })\n"); - } else { - lua.push_str(" return dofile(\""); - lua.push_str(&resources.init.to_slash_lossy()); - lua.push_str("\")\n"); - } - - lua.push_str(" end,\n packages = {\n"); - - 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"); +fn build_mod_data_lua(state: Arc) -> Result { + #[derive(Serialize)] + struct TemplateDataMod { + id: String, + name: String, + bundled: bool, + init: String, + data: Option, + localization: Option, + packages: Vec, } - lua.push('}'); + 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`")?; - tracing::debug!("mod_data_lua:\n{}", lua); + let data: Vec = state + .mods + .iter() + .filter_map(|m| { + if m.id == "dml" || !m.enabled { + return None; + } - lua + 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(); + + let lua = tmpl + .render(minijinja::context!(mods => data)) + .wrap_err("Failed to render template `mod_data.lua`")?; + + tracing::debug!("mod_data.lua:\n{}", lua); + + Ok(lua) } #[tracing::instrument(skip_all)] @@ -340,7 +333,7 @@ async fn build_bundles(state: Arc) -> Result> { 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");