diff --git a/crates/lovely-core/benches/patches.rs b/crates/lovely-core/benches/patches.rs index 1f8badf..d90e0aa 100644 --- a/crates/lovely-core/benches/patches.rs +++ b/crates/lovely-core/benches/patches.rs @@ -39,7 +39,7 @@ fn pattern_patches_no_match() -> &'static [PatternPatch] { match_indent: false, times: None, overwrite: false, - name: None, + name: None, silent: false, }, PatternPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -49,7 +49,7 @@ fn pattern_patches_no_match() -> &'static [PatternPatch] { match_indent: false, times: None, overwrite: false, - name: None, + name: None, silent: false, }, PatternPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -59,7 +59,7 @@ fn pattern_patches_no_match() -> &'static [PatternPatch] { match_indent: false, times: None, overwrite: false, - name: None, + name: None, silent: false, }, PatternPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -69,7 +69,7 @@ fn pattern_patches_no_match() -> &'static [PatternPatch] { match_indent: false, times: None, overwrite: false, - name: None, + name: None, silent: false, }, ] }) @@ -87,7 +87,7 @@ fn pattern_patches_with_match() -> &'static [PatternPatch] { match_indent: false, times: Some(1), overwrite: false, - name: None, + name: None, silent: false, }, PatternPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -97,7 +97,7 @@ fn pattern_patches_with_match() -> &'static [PatternPatch] { match_indent: false, times: Some(5), overwrite: false, - name: None, + name: None, silent: false, }, PatternPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -107,7 +107,7 @@ fn pattern_patches_with_match() -> &'static [PatternPatch] { match_indent: false, times: Some(1), overwrite: false, - name: None, + name: None, silent: false, }, PatternPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -117,7 +117,7 @@ fn pattern_patches_with_match() -> &'static [PatternPatch] { match_indent: false, times: Some(2), overwrite: false, - name: None, + name: None, silent: false, }, ] }) @@ -136,7 +136,7 @@ fn regex_patches_no_match() -> &'static [RegexPatch] { line_prepend: String::new(), times: None, verbose: false, - name: None, + name: None, silent: false, }, RegexPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -147,7 +147,7 @@ fn regex_patches_no_match() -> &'static [RegexPatch] { line_prepend: String::new(), times: None, verbose: false, - name: None, + name: None, silent: false, }, RegexPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -158,7 +158,7 @@ fn regex_patches_no_match() -> &'static [RegexPatch] { line_prepend: String::new(), times: None, verbose: false, - name: None, + name: None, silent: false, }, RegexPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -169,7 +169,7 @@ fn regex_patches_no_match() -> &'static [RegexPatch] { line_prepend: String::new(), times: None, verbose: false, - name: None, + name: None, silent: false, }, ] }) @@ -188,7 +188,7 @@ fn regex_patches_with_match() -> &'static [RegexPatch] { line_prepend: String::new(), times: Some(1), verbose: false, - name: None, + name: None, silent: false, }, RegexPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -199,7 +199,7 @@ fn regex_patches_with_match() -> &'static [RegexPatch] { line_prepend: String::new(), times: Some(5), verbose: false, - name: None, + name: None, silent: false, }, RegexPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -210,7 +210,7 @@ fn regex_patches_with_match() -> &'static [RegexPatch] { line_prepend: String::new(), times: Some(1), verbose: false, - name: None, + name: None, silent: false, }, RegexPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -221,7 +221,7 @@ fn regex_patches_with_match() -> &'static [RegexPatch] { line_prepend: String::new(), times: Some(2), verbose: false, - name: None, + name: None, silent: false, }, ] }) @@ -239,7 +239,7 @@ fn pattern_patches_position() -> &'static [PatternPatch] { match_indent: false, times: Some(1), overwrite: false, - name: None, + name: None, silent: false, }, PatternPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -249,7 +249,7 @@ fn pattern_patches_position() -> &'static [PatternPatch] { match_indent: false, times: Some(1), overwrite: false, - name: None, + name: None, silent: false, }, PatternPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -259,7 +259,7 @@ fn pattern_patches_position() -> &'static [PatternPatch] { match_indent: false, times: Some(1), overwrite: false, - name: None, + name: None, silent: false, }, ] }) @@ -278,7 +278,7 @@ fn regex_patches_position() -> &'static [RegexPatch] { line_prepend: String::new(), times: Some(1), verbose: false, - name: None, + name: None, silent: false, }, RegexPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -289,7 +289,7 @@ fn regex_patches_position() -> &'static [RegexPatch] { line_prepend: String::new(), times: Some(1), verbose: false, - name: None, + name: None, silent: false, }, RegexPatch { target: Target::Single("sample_buffer.txt".to_string()), @@ -300,7 +300,7 @@ fn regex_patches_position() -> &'static [RegexPatch] { line_prepend: String::new(), times: Some(1), verbose: false, - name: None, + name: None, silent: false, }, ] }) diff --git a/crates/lovely-core/src/lib.rs b/crates/lovely-core/src/lib.rs index 5186647..7ac9ced 100644 --- a/crates/lovely-core/src/lib.rs +++ b/crates/lovely-core/src/lib.rs @@ -17,9 +17,10 @@ use patch::{ModulePatch, Patch}; use regex_lite::Regex; use sys::{check_lua_string, LuaFunc, LuaLib, LuaState, LuaStateTrait, LUA}; +use wildmatch::WildMatch; +use crate::dump::{write_dump, PatchDebug}; use crate::patch::Target; -use crate::dump::{PatchDebug, write_dump}; pub mod chunk_vec_cursor; pub mod dump; @@ -311,16 +312,23 @@ impl Lovely { // Apply patches onto this buffer. let res = patch_table.apply_patches(name, buf_str, state); - if res.is_err() { - state.push(res.unwrap_err()); + if let Err(err) = res { + state.push(err); // NOTE: Not really a great error but it doesn't handle the correcter errors right. return 3; // LUA_ERRSYNTAX } let (patched, debug) = res.unwrap(); - write_dump(&self.mod_dir, "game-dump", &pretty_name, &patched, &PatchDebug::new(name)); - write_dump(&self.mod_dir, "dump", &pretty_name, &patched, &debug); - + if self.dump_all || !debug.entries.iter().all(|x| x.regions.is_empty()) { + write_dump( + &self.mod_dir, + "game-dump", + &pretty_name, + &patched, + &PatchDebug::new(name), + ); + write_dump(&self.mod_dir, "dump", &pretty_name, &patched, &debug); + } (self.loadbuffer)(state, patched.as_ptr(), patched.len(), name_ptr, mode_ptr) } } @@ -336,9 +344,9 @@ unsafe extern "C" fn apply_patches(lua_state: *mut LuaState) -> c_int { let binding = RUNTIME.get().unwrap().patch_table.read().unwrap(); if binding.needs_patching(&buf_name) { let res = binding.apply_patches(&buf_name, &buf, lua_state); - if res.is_err() { + if let Err(err) = res { lua_state.push(false); - lua_state.push(res.unwrap_err()); + lua_state.push(err); num = 2; return; } @@ -360,19 +368,27 @@ unsafe extern "C" fn apply_patches(lua_state: *mut LuaState) -> c_int { impl Target { pub fn can_apply(&self, target: &str) -> bool { match self { - Self::Single(str) => str == target, - Self::Multi(strs) => strs.iter().any(|x| x == target), + Self::Single(str) => WildMatch::new(str).matches(target), + Self::Multi(strs) => strs.iter().any(|x| WildMatch::new(x).matches(target)), } } - pub fn insert_into(&self, targets: &mut HashSet) { + pub fn insert_into(&self, targets: &mut (HashSet, Vec)) { match self { Self::Single(str) => { - targets.insert(str.clone()); + if str.contains('?') || str.contains('*') { + targets.1.push(WildMatch::new(str)); + } else { + targets.0.insert(str.clone()); + } } Self::Multi(strs) => { for target in strs.iter() { - targets.insert(target.clone()); + if target.contains('?') || target.contains('*') { + targets.1.push(WildMatch::new(target)); + } else { + targets.0.insert(target.clone()); + } } } } diff --git a/crates/lovely-core/src/patch/loader.rs b/crates/lovely-core/src/patch/loader.rs index 418f4d4..768aae6 100644 --- a/crates/lovely-core/src/patch/loader.rs +++ b/crates/lovely-core/src/patch/loader.rs @@ -4,8 +4,9 @@ use std::collections::{HashMap, HashSet}; use std::fs; use std::io::Read; use std::path::{Path, PathBuf}; +use wildmatch::WildMatch; -use crate::patch::{Patch, PatchFile, Priority}; +use crate::patch::{Patch, PatchFile, Priority, Target}; use itertools::Itertools; use log::*; use walkdir::WalkDir; @@ -68,7 +69,9 @@ fn get_dir_patches(mod_dir: &Path) -> Result<(PathBuf, Vec)> } } Patch::Copy(x) => { - let Some(ref copy_sources) = x.sources else { continue }; + let Some(ref copy_sources) = x.sources else { + continue; + }; for source in copy_sources { let full_path = mod_dir.join(source); if let Ok(source_content) = fs::read_to_string(&full_path) { @@ -105,31 +108,28 @@ fn get_zip_patches(zip_file: &Path) -> Result<(PathBuf, Vec)> .collect(); // Find the mod root. This is the dir that contains lovely.toml or lovely/ - let mod_root = names - .iter() - .find_map(|name| { - let parent = get_parent(name); - let lovely_toml = format!("{}lovely.toml", parent); - let lovely_dir = format!("{}lovely/", parent); - - if names - .iter() - .any(|n| n == &lovely_toml || n.starts_with(&lovely_dir)) - { - Some(parent) - } else { - None - } - }); + let mod_root = names.iter().find_map(|name| { + let parent = get_parent(name); + let lovely_toml = format!("{}lovely.toml", parent); + let lovely_dir = format!("{}lovely/", parent); + + if names + .iter() + .any(|n| n == &lovely_toml || n.starts_with(&lovely_dir)) + { + Some(parent) + } else { + None + } + }); let mod_root = match mod_root { - Some(v) => v, - None => { - log::warn!("No mod root found in zip {:?}. This may happen if the mod does not contain any lovely patches (uses another loader)", zip_file); - return Ok((zip_file.to_path_buf(), Vec::new())); - }, + Some(v) => v, + None => { + log::warn!("No mod root found in zip {:?}. This may happen if the mod does not contain any lovely patches (uses another loader)", zip_file); + return Ok((zip_file.to_path_buf(), Vec::new())); + } }; - let lovely_toml_path = format!("{}lovely.toml", mod_root); let lovely_dir_prefix = format!("{}lovely/", mod_root); @@ -161,7 +161,10 @@ fn get_zip_patches(zip_file: &Path) -> Result<(PathBuf, Vec)> let mut content = String::new(); file.read_to_string(&mut content).with_context(|| { - format!("Failed to read contents of {} from zip {:?}", toml_path, zip_file) + format!( + "Failed to read contents of {} from zip {:?}", + toml_path, zip_file + ) })?; // Parse TOML to find referenced module/copy sources @@ -176,7 +179,8 @@ fn get_zip_patches(zip_file: &Path) -> Result<(PathBuf, Vec)> Patch::Copy(x) => { if let Some(ref sources) = x.sources { for source in sources { - let source_path = format!("{}{}", mod_root, source.to_string_lossy()); + let source_path = + format!("{}{}", mod_root, source.to_string_lossy()); source_paths.insert(source_path); } } @@ -195,7 +199,10 @@ fn get_zip_patches(zip_file: &Path) -> Result<(PathBuf, Vec)> if let Ok(mut file) = zip.by_name(source_path) { let mut content = String::new(); file.read_to_string(&mut content).with_context(|| { - format!("Failed to read source {} from zip {:?}", source_path, zip_file) + format!( + "Failed to read source {} from zip {:?}", + source_path, zip_file + ) })?; file_contents.insert(source_path.clone(), content); } @@ -231,10 +238,12 @@ fn get_zip_patches(zip_file: &Path) -> Result<(PathBuf, Vec)> /// within each subdirectory that matches either: /// - MOD_DIR/lovely.toml /// - MOD_DIR/lovely/*.toml -/// -/// Zip archives are supported and uniquely support directory nesting +/// +/// Zip archives are supported and uniquely support directory nesting /// (i.e., mod.zip/dir/lovely.toml), but otherwise are treated the same as dir mods. -pub fn load_patches_new(mod_dir: &Path) -> Result)>> { +pub fn load_patches_new( + mod_dir: &Path, +) -> Result)>> { let blacklist_file = mod_dir.join("lovely").join("blacklist.txt"); let mut blacklist: HashSet = HashSet::new(); @@ -302,7 +311,8 @@ pub fn load_patches_new(mod_dir: &Path) -> Result Result Result, HashSet, HashMap, + Vec, ) { - let mut targets: HashSet = HashSet::new(); + let mut targets: (HashSet, Vec) = (HashSet::new(), Vec::new()); let mut patches: Vec<(Patch, Priority, PathBuf)> = Vec::new(); let mut var_table: HashMap = HashMap::new(); - for (patch, priority, path, vars) in raw_patches { + for (mut patch, priority, path, vars) in raw_patches { // Extract targets from patches - match &patch { + match &mut patch { Patch::Copy(x) => { x.target.insert_into(&mut targets); } Patch::Module(x) => { - targets.insert(x.before.clone().unwrap_or_default()); + targets.0.insert(x.before.clone().unwrap_or_default()); } Patch::Pattern(x) => { + let should_override_silent = match &x.target { + Target::Single(str) => str == "*", + Target::Multi(strs) => strs.iter().any(|x| x == "*"), + }; + x.silent = x.silent || should_override_silent; + x.target.insert_into(&mut targets); } Patch::Regex(x) => { + let should_override_silent = match &x.target { + Target::Single(str) => str == "*", + Target::Multi(strs) => strs.iter().any(|x| x == "*"), + }; + x.silent = x.silent || should_override_silent; + x.target.insert_into(&mut targets); } } @@ -399,7 +435,7 @@ pub fn process_patches( var_table.extend(vars); } - (patches, targets, var_table) + (patches, targets.0, var_table, targets.1) } /// Parse TOML content into a PatchFile @@ -416,8 +452,6 @@ fn parse_patch_file(content: &str, file_identifier: &str, mod_dir: &Path) -> Res .with_context(|| format!("Failed to parse patch file {file_identifier}")) } - - /// Helper to extract parent directory path with trailing slash fn get_parent(path: &str) -> String { path.rfind('/') @@ -467,35 +501,49 @@ sources = ["inject.lua"] let (_, patches) = get_dir_patches(&mod_dir).unwrap(); assert_eq!(patches.len(), 1); - assert_eq!(patches[0].sources.get(Path::new("inject.lua")).unwrap(), "-- injected"); + assert_eq!( + patches[0].sources.get(Path::new("inject.lua")).unwrap(), + "-- injected" + ); } #[test] fn zip_patches_preloads_sources() { let temp = TempDir::new().unwrap(); - let zip = make_zip(&temp, "mod.zip", &[ - ("lovely.toml", PATCH_TOML), - ("inject.lua", "-- from zip"), - ]); + let zip = make_zip( + &temp, + "mod.zip", + &[("lovely.toml", PATCH_TOML), ("inject.lua", "-- from zip")], + ); let (_, patches) = get_zip_patches(&zip).unwrap(); assert_eq!(patches.len(), 1); - assert_eq!(patches[0].sources.get(Path::new("inject.lua")).unwrap(), "-- from zip"); + assert_eq!( + patches[0].sources.get(Path::new("inject.lua")).unwrap(), + "-- from zip" + ); } #[test] fn zip_nested_mod_root() { let temp = TempDir::new().unwrap(); - let zip = make_zip(&temp, "mod.zip", &[ - ("Nested/lovely.toml", PATCH_TOML), - ("Nested/inject.lua", "-- nested"), - ]); + let zip = make_zip( + &temp, + "mod.zip", + &[ + ("Nested/lovely.toml", PATCH_TOML), + ("Nested/inject.lua", "-- nested"), + ], + ); let (_, patches) = get_zip_patches(&zip).unwrap(); assert_eq!(patches.len(), 1); - assert_eq!(patches[0].sources.get(Path::new("inject.lua")).unwrap(), "-- nested"); + assert_eq!( + patches[0].sources.get(Path::new("inject.lua")).unwrap(), + "-- nested" + ); } #[test] @@ -514,7 +562,8 @@ sources = ["inject.lua"] fs::write( mods.join("lovely/blacklist.txt"), "blocked_dir\nblocked.zip\n# comment\n\n", - ).unwrap(); + ) + .unwrap(); // Allowed dir mod let allowed = mods.join("allowed"); @@ -529,10 +578,11 @@ sources = ["inject.lua"] fs::write(blocked.join("inject.lua"), "").unwrap(); // Blocked zip mod - make_zip(&temp, "blocked.zip", &[ - ("lovely.toml", PATCH_TOML), - ("inject.lua", ""), - ]); + make_zip( + &temp, + "blocked.zip", + &[("lovely.toml", PATCH_TOML), ("inject.lua", "")], + ); fs::rename(temp.path().join("blocked.zip"), mods.join("blocked.zip")).unwrap(); let patches = load_patches_new(mods).unwrap(); @@ -581,7 +631,9 @@ sources = ["inject.lua"] let m = mods.join("mod"); fs::create_dir_all(m.join("lovely")).unwrap(); - fs::write(m.join("lovely/patch1.toml"), r#" + fs::write( + m.join("lovely/patch1.toml"), + r#" [manifest] version = "1.0.0" @@ -590,8 +642,12 @@ version = "1.0.0" target = "a.lua" position = "append" payload = "-- a" -"#).unwrap(); - fs::write(m.join("lovely/patch2.toml"), r#" +"#, + ) + .unwrap(); + fs::write( + m.join("lovely/patch2.toml"), + r#" [manifest] version = "1.0.0" @@ -600,7 +656,9 @@ version = "1.0.0" target = "b.lua" position = "append" payload = "-- b" -"#).unwrap(); +"#, + ) + .unwrap(); let patches = load_patches_new(mods).unwrap(); assert_eq!(patches.len(), 2); @@ -614,7 +672,9 @@ payload = "-- b" let m = mods.join("mod"); fs::create_dir_all(&m).unwrap(); - fs::write(m.join("lovely.toml"), r#" + fs::write( + m.join("lovely.toml"), + r#" [manifest] version = "1.0.0" @@ -626,10 +686,13 @@ FOO = "bar" target = "game.lua" position = "append" payload = "-- hi" -"#).unwrap(); +"#, + ) + .unwrap(); let raw = load_patches_new(mods).unwrap(); - let (patches, targets, vars) = process_patches(raw); + //todo: account for globs in this test + let (patches, targets, vars, _) = process_patches(raw); assert_eq!(patches.len(), 1); assert!(targets.contains("game.lua")); diff --git a/crates/lovely-core/src/patch/pattern.rs b/crates/lovely-core/src/patch/pattern.rs index 4297f19..c29525d 100644 --- a/crates/lovely-core/src/patch/pattern.rs +++ b/crates/lovely-core/src/patch/pattern.rs @@ -30,6 +30,11 @@ pub struct PatternPatch { #[serde(default)] pub overwrite: bool, + // Whether a warning will appear if the patch does not match anything. + // Overridden to true for targets that are simply "*" + #[serde(default)] + pub silent: bool, + // Currently unused. pub name: Option, } @@ -97,7 +102,7 @@ impl PatternPatch { } } - if matches.is_empty() { + if matches.is_empty() && !self.silent { return Some(self.debug_from_warning_string(path, format!( "Pattern '{}' on target '{target}' for pattern patch from {} resulted in no matches", self.pattern.escape_debug(), diff --git a/crates/lovely-core/src/patch/regex.rs b/crates/lovely-core/src/patch/regex.rs index 50532d5..8918632 100644 --- a/crates/lovely-core/src/patch/regex.rs +++ b/crates/lovely-core/src/patch/regex.rs @@ -32,6 +32,11 @@ pub struct RegexPatch { // by $index. pub payload: String, + // Whether a warning will appear if the patch does not match anything. + // Overridden to true for targets that are simply "*" + #[serde(default)] + pub silent: bool, + // A string or Regex capture to prepend onto the start of each LINE of the payload. // This value defaults to an empty string. #[serde(default)] @@ -85,7 +90,7 @@ impl RegexPatch { }); let mut captures = re.captures_iter(input).collect_vec(); - if captures.is_empty() { + if captures.is_empty() && !self.silent { let warning = format!("Regex '{}' on target '{target}' for regex patch from {} resulted in no matches", self.pattern.escape_debug(), path.display()); return Some(self.debug_from_warning_string(path, warning)); } diff --git a/crates/lovely-core/src/patch/table.rs b/crates/lovely-core/src/patch/table.rs index 2dc5c7a..f883f38 100644 --- a/crates/lovely-core/src/patch/table.rs +++ b/crates/lovely-core/src/patch/table.rs @@ -1,6 +1,7 @@ use anyhow::Result; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; +use wildmatch::WildMatch; use crate::dump::{ByteDebugEntry, PatchDebug}; use crate::patch::{loader, vars}; @@ -14,6 +15,7 @@ use log::*; pub struct PatchTable { pub mod_dir: PathBuf, pub targets: HashSet, + pub glob_targets: Vec, // Unsorted pub patches: Vec<(Patch, Priority, PathBuf)>, pub vars: HashMap, @@ -25,6 +27,7 @@ impl Default for PatchTable { Self { mod_dir: PathBuf::new(), targets: HashSet::new(), + glob_targets: Vec::new(), patches: Vec::new(), vars: HashMap::new(), } @@ -35,11 +38,12 @@ impl PatchTable { /// Load patches from the provided mod directory. pub fn load(mod_dir: &Path) -> Result { let raw_patches = loader::load_patches_new(mod_dir)?; - let (patches, targets, vars) = loader::process_patches(raw_patches); + let (patches, targets, vars, glob_targets) = loader::process_patches(raw_patches); Ok(PatchTable { mod_dir: mod_dir.to_path_buf(), targets, + glob_targets, patches, vars, }) @@ -48,7 +52,11 @@ impl PatchTable { /// Determine if the provided target file / name requires patching. pub fn needs_patching(&self, target: &str) -> bool { let target = target.strip_prefix('@').unwrap_or(target); - self.targets.contains(target) + if self.targets.contains(target) { + true + } else { + self.glob_targets.iter().any(|x| x.matches(target)) + } } /// Inject lovely metadata into the game. @@ -86,7 +94,8 @@ impl PatchTable { target: &str, buffer: &str, lua_state: *mut LuaState, - ) -> Result<(String, PatchDebug), String> { // Buffer Content, Debug info, Error message + ) -> Result<(String, PatchDebug), String> { + // Buffer Content, Debug info, Error message let target = target.strip_prefix('@').unwrap_or(target); let module_patches = self @@ -170,7 +179,9 @@ impl PatchTable { } } - patch_count += 1; + if !entry.regions.is_empty() { + patch_count += 1 + }; byte_entries.push(entry); } } @@ -194,7 +205,7 @@ impl PatchTable { if patch_count == 1 { info!("Applied 1 patch to '{target}'"); - } else { + } else if patch_count >= 2 { info!("Applied {patch_count} patches to '{target}'"); }