1use getset::{Getters, Setters};
25use mlua::{HookTriggers, Lua, LuaOptions, StdLib, Table, VmState};
26use serde_derive::{Deserialize, Serialize};
27
28use std::cell::Cell;
29use std::collections::HashMap;
30use std::rc::Rc;
31use std::sync::Arc;
32
33use rpfm_lib::error::{RLibError, Result};
34use rpfm_lib::files::{Container, FileType, pack::Pack, RFileDecoded};
35
36use crate::dependencies::Dependencies;
37
38use super::{vanilla_scripts, LuaApi, LuaType};
39
40#[cfg(test)] mod tests;
41
42const ENGINE_SCRIPT: &str = include_str!("engine.lua");
44
45const CAMPAIGN_BOOT_SCRIPT: &str = "script/campaign_scripted.lua";
47
48const LEGACY_CAMPAIGN_BOOT_SCRIPT: &str = "lua_scripts/campaign_scripted.lua";
50
51const SAVING_GAME_EVENT: &str = "SavingGame";
53
54const TEST_FILE_CHUNK: &str = "@test";
56
57const INSTRUCTION_LIMIT: u64 = 2_000_000_000;
59
60const INSTRUCTION_CHECK_INTERVAL: u32 = 100_000;
62
63#[derive(Clone, Debug, Default)]
71pub struct LuaScripts {
72
73 scripts: Arc<HashMap<String, (String, String)>>,
75
76 pack_paths: Arc<Vec<String>>,
78}
79
80#[derive(Clone, Debug, Default, PartialEq, Getters, Setters, Serialize, Deserialize)]
82#[getset(get = "pub", set = "pub")]
83pub struct LuaTestOptions {
84
85 campaign: Option<String>,
88
89 faction_keys: Vec<String>,
91
92 region_keys: Vec<String>,
94}
95
96#[derive(Clone, Debug, Default, PartialEq, Getters, Serialize, Deserialize)]
98#[getset(get = "pub")]
99pub struct LuaTestReport {
100
101 tests: Vec<LuaTestResult>,
103
104 boot_errors: Vec<String>,
107}
108
109#[derive(Clone, Debug, Default, PartialEq, Getters, Serialize, Deserialize)]
111#[getset(get = "pub")]
112pub struct LuaSave {
113
114 values: Vec<(String, LuaSavedValue)>,
116
117 errors: Vec<String>,
119}
120
121#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub enum LuaSavedValue {
124 Bool(bool),
125 Integer(i32),
126 String(String),
127}
128
129#[derive(Clone, Debug, Default, PartialEq, Getters, Serialize, Deserialize)]
131#[getset(get = "pub")]
132pub struct LuaTestResult {
133
134 name: String,
136
137 passed: bool,
139
140 errors: Vec<String>,
143
144 unmocked_calls: Vec<String>,
147
148 log: Vec<String>,
150}
151
152impl LuaScripts {
157
158 pub fn from_game_and_packs<'a>(dependencies: &Dependencies, packs: impl IntoIterator<Item = &'a Pack>) -> Self {
169 let mut scripts = Self::default();
170 for (path, source) in vanilla_scripts(dependencies) {
171 scripts.add(&path, source, false);
172 }
173
174 for file in packs.into_iter().flat_map(|pack| pack.files_by_type(&[FileType::Text])) {
175 let path = file.path_in_container_raw();
176 if !path.ends_with(".lua") {
177 continue;
178 }
179
180 let source = match file.decoded() {
182 Ok(RFileDecoded::Text(text)) => Some(text.contents().to_owned()),
183 _ => {
184 let mut file = file.clone();
185 file.load().ok().and_then(|_| file.cached().ok().map(|data| String::from_utf8_lossy(data).to_string()))
186 }
187 };
188
189 if let Some(source) = source {
190 scripts.add(path, source, true);
191 }
192 }
193
194 scripts
195 }
196
197 pub fn add(&mut self, path: &str, source: String, from_tested_pack: bool) {
205 let path = path.replace('\\', "/");
206 let key = path.to_lowercase();
207 let source = source.strip_prefix('\u{feff}').map(str::to_owned).unwrap_or(source);
208 if from_tested_pack {
209 Arc::make_mut(&mut self.pack_paths).push(key.to_owned());
210 }
211
212 Arc::make_mut(&mut self.scripts).insert(key, (path, source));
213 }
214
215 fn get(&self, path: &str) -> Option<&(String, String)> {
217 self.scripts.get(&path.to_lowercase())
218 }
219
220 pub fn contains(&self, path: &str) -> bool {
222 self.get(path).is_some()
223 }
224
225 fn has_campaign_boot_script(&self) -> bool {
227 self.contains(CAMPAIGN_BOOT_SCRIPT) || self.contains(LEGACY_CAMPAIGN_BOOT_SCRIPT)
228 }
229
230 fn list(&self, folder: &str, filter: &str) -> String {
241 let folder = folder.trim_matches('/').to_lowercase() + "/";
242 let extension = filter.strip_prefix("*.").filter(|extension| *extension != "*").map(|extension| format!(".{}", extension.to_lowercase()));
243
244 let mut paths = self.scripts.iter()
245 .filter(|(key, _)| key.strip_prefix(&folder).is_some_and(|name| !name.contains('/')))
246 .filter(|(key, _)| extension.as_ref().is_none_or(|extension| key.ends_with(extension)))
247 .map(|(_, (path, _))| path.as_str())
248 .collect::<Vec<_>>();
249
250 paths.sort_unstable();
251 paths.join(",")
252 }
253
254 fn is_pack_error(&self, error: &str) -> bool {
258 let error = error.to_lowercase();
259 self.pack_paths.iter().any(|path| error.contains(path.as_str()))
260 }
261}
262
263pub fn run_tests(api: &LuaApi, scripts: &LuaScripts, test_source: &str, options: &LuaTestOptions) -> Result<LuaTestReport> {
280 if !scripts.has_campaign_boot_script() {
281 return Err(RLibError::LuaTestGameScriptsNotFound);
282 }
283
284 let mut report = LuaTestReport::default();
285 let mut index = 1;
286
287 loop {
289 let lua = new_state(api, scripts, options)?;
290 load_test_file(&lua, test_source)?;
291
292 let rpfm: Table = lua.globals().get("rpfm").map_err(lua_error)?;
293 let tests: Table = rpfm.get("tests").map_err(lua_error)?;
294 if index > tests.raw_len() {
295 break;
296 }
297
298 let boot: mlua::Function = rpfm.get("__boot").map_err(lua_error)?;
299 let boot_error = boot.call::<Option<String>>(options.campaign.as_deref()).map_err(lua_error)?;
300
301 let (pack_errors, other_errors): (Vec<_>, Vec<_>) = string_list(&rpfm, "errors")?.into_iter().partition(|error| scripts.is_pack_error(error));
302 if index == 1 {
303 report.boot_errors = other_errors;
304 }
305
306 let test: Table = tests.get(index).map_err(lua_error)?;
307 let name: String = test.get("name").map_err(lua_error)?;
308
309 let run_test: mlua::Function = rpfm.get("__run_test").map_err(lua_error)?;
310 let (passed, test_error): (bool, Option<String>) = match run_test.call::<(bool, Option<String>)>(index) {
311 Ok(result) => result,
312 Err(error) => (false, Some(error.to_string())),
313 };
314
315 let mut errors = boot_error.map(|error| vec![format!("The game's script libraries failed to load: {error}")]).unwrap_or_default();
317 errors.extend(pack_errors);
318 errors.extend(test_error);
319 errors.extend(string_list(&rpfm, "errors")?);
320
321 let mut unmocked_calls = rpfm.get::<Table>("unmocked").map_err(lua_error)?
322 .pairs::<String, bool>()
323 .filter_map(|pair| pair.ok().map(|(call, _)| call))
324 .collect::<Vec<_>>();
325 unmocked_calls.sort_unstable();
326
327 report.tests.push(LuaTestResult {
328 name,
329 passed: passed && errors.is_empty(),
330 errors,
331 unmocked_calls,
332 log: string_list(&rpfm, "log")?,
333 });
334
335 index += 1;
336 }
337
338 Ok(report)
339}
340
341pub fn save_campaign(api: &LuaApi, scripts: &LuaScripts, options: &LuaTestOptions, boot_events: &[&str]) -> Result<LuaSave> {
358 if !scripts.has_campaign_boot_script() {
359 return Err(RLibError::LuaTestGameScriptsNotFound);
360 }
361
362 let lua = new_state(api, scripts, options)?;
363 let rpfm: Table = lua.globals().get("rpfm").map_err(lua_error)?;
364 let boot: mlua::Function = rpfm.get("__boot").map_err(lua_error)?;
365 if let Some(error) = boot.call::<Option<String>>((options.campaign.as_deref(), boot_events.to_vec())).map_err(lua_error)? {
366 return Err(RLibError::LuaRuntimeError(format!("the game's script libraries failed to load: {error}")));
367 }
368
369 let fire: mlua::Function = rpfm.get("fire").map_err(lua_error)?;
370 fire.call::<()>(SAVING_GAME_EVENT).map_err(lua_error)?;
371
372 let mut values = vec![];
373 for entry in rpfm.get::<Table>("saved_values").map_err(lua_error)?.sequence_values::<Table>() {
374 let entry = entry.map_err(lua_error)?;
375 let name: String = entry.get("name").map_err(lua_error)?;
376
377 let value = match entry.get::<mlua::Value>("value").map_err(lua_error)? {
379 mlua::Value::Boolean(value) => LuaSavedValue::Bool(value),
380 mlua::Value::Integer(value) => LuaSavedValue::Integer(value as i32),
381 mlua::Value::Number(value) => LuaSavedValue::Integer(value.round_ties_even() as i32),
382 mlua::Value::String(value) => LuaSavedValue::String(String::from_utf8_lossy(&value.as_bytes()).into_owned()),
383 _ => continue,
384 };
385 values.push((name, value));
386 }
387
388 Ok(LuaSave {
389 values,
390 errors: string_list(&rpfm, "errors")?,
391 })
392}
393
394fn new_state(api: &LuaApi, scripts: &LuaScripts, options: &LuaTestOptions) -> Result<Lua> {
396
397 let lua = unsafe { Lua::unsafe_new_with(StdLib::ALL_SAFE | StdLib::DEBUG, LuaOptions::default()) };
400
401 let instructions = Rc::new(Cell::new(0u64));
402 lua.set_hook(HookTriggers::new().every_nth_instruction(INSTRUCTION_CHECK_INTERVAL), move |_, _| {
403 instructions.set(instructions.get() + u64::from(INSTRUCTION_CHECK_INTERVAL));
404 if instructions.get() > INSTRUCTION_LIMIT {
405 return Err(mlua::Error::runtime("the scripts ran for too long, they may be stuck in a loop"));
406 }
407 Ok(VmState::Continue)
408 }).map_err(lua_error)?;
409
410 let globals = lua.globals();
411 let read_scripts = scripts.clone();
412 let read_script = lua.create_function(move |_, path: String| Ok(read_scripts.get(&path).map(|(_, source)| source.to_owned()))).map_err(lua_error)?;
413 let list_scripts_map = scripts.clone();
414 let list_scripts = lua.create_function(move |_, (folder, filter): (String, String)| Ok(list_scripts_map.list(&folder, &filter))).map_err(lua_error)?;
415
416 globals.set("rpfm_read_script", read_script).map_err(lua_error)?;
417 globals.set("rpfm_list_scripts", list_scripts).map_err(lua_error)?;
418 globals.set("rpfm_returns", returns_table(&lua, api)?).map_err(lua_error)?;
419 globals.set("rpfm_accessors", accessors_table(&lua, api)?).map_err(lua_error)?;
420 globals.set("rpfm_faction_keys", key_set(&lua, options.faction_keys())?).map_err(lua_error)?;
421 globals.set("rpfm_region_keys", key_set(&lua, options.region_keys())?).map_err(lua_error)?;
422
423 lua.load(ENGINE_SCRIPT).set_name("@engine").exec().map_err(lua_error)?;
424 Ok(lua)
425}
426
427fn load_test_file(lua: &Lua, test_source: &str) -> Result<()> {
429 lua.load(test_source).set_name(TEST_FILE_CHUNK).exec().map_err(|error| RLibError::LuaTestFileError(error.to_string()))
430}
431
432fn returns_table(lua: &Lua, api: &LuaApi) -> Result<Table> {
437 let owners = lua.create_table().map_err(lua_error)?;
438 for (owner, functions) in api.owners() {
439 let methods = lua.create_table().map_err(lua_error)?;
440 for (name, function) in functions {
441 let return_type = function.returns().first().map_or_else(|| "nil".to_owned(), |returned| type_name(api, returned.lua_type()));
442 methods.set(name.as_str(), return_type).map_err(lua_error)?;
443 }
444 owners.set(owner.as_str(), methods).map_err(lua_error)?;
445 }
446
447 Ok(owners)
448}
449
450fn accessors_table(lua: &Lua, api: &LuaApi) -> Result<Table> {
452 let events = lua.create_table().map_err(lua_error)?;
453 for (event, accessors) in api.events() {
454 let types = lua.create_table().map_err(lua_error)?;
455 for (name, accessor) in accessors {
456 types.set(name.as_str(), type_name(api, accessor.lua_type())).map_err(lua_error)?;
457 }
458 events.set(event.as_str(), types).map_err(lua_error)?;
459 }
460
461 Ok(events)
462}
463
464fn key_set(lua: &Lua, keys: &[String]) -> Result<Table> {
466 let set = lua.create_table_with_capacity(0, keys.len()).map_err(lua_error)?;
467 for key in keys {
468 set.set(key.as_str(), true).map_err(lua_error)?;
469 }
470
471 Ok(set)
472}
473
474fn type_name(api: &LuaApi, lua_type: &LuaType) -> String {
476 match lua_type {
477 LuaType::Object(object) => {
478 let interface = format!("{}_SCRIPT_INTERFACE", object.to_uppercase());
479 if api.owners().contains_key(&interface) { interface } else { "any".to_owned() }
480 }
481 _ => lua_type.name().to_owned(),
482 }
483}
484
485fn string_list(table: &Table, field: &str) -> Result<Vec<String>> {
487 let list: Table = table.get(field).map_err(lua_error)?;
488 list.sequence_values::<String>().collect::<mlua::Result<Vec<_>>>().map_err(lua_error)
489}
490
491fn lua_error(error: mlua::Error) -> RLibError {
493 RLibError::LuaRuntimeError(error.to_string())
494}