mirror of
https://github.com/loot/loot-condition-interpreter.git
synced 2026-07-27 14:16:09 -07:00
Add support for additional data paths
The Microsoft Store installs Fallout 4 DLCs into separate game directories, so allow them to be provided for evaluating conditions.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
use std::ffi::{CStr, CString};
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::slice;
|
use std::slice;
|
||||||
|
|
||||||
use libc::{c_char, c_int, size_t};
|
use libc::{c_char, c_int, size_t};
|
||||||
@@ -76,6 +77,13 @@ pub unsafe fn to_str_vec<'a>(
|
|||||||
to_vec(array, array_size, |c| to_str(*c))
|
to_vec(array, array_size, |c| to_str(*c))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub unsafe fn to_path_buf_vec(
|
||||||
|
array: *const *const c_char,
|
||||||
|
array_size: size_t,
|
||||||
|
) -> Result<Vec<PathBuf>, i32> {
|
||||||
|
to_vec(array, array_size, |c| to_str(*c).map(PathBuf::from))
|
||||||
|
}
|
||||||
|
|
||||||
unsafe fn map_plugin_version(c_object: &plugin_version) -> Result<(String, String), i32> {
|
unsafe fn map_plugin_version(c_object: &plugin_version) -> Result<(String, String), i32> {
|
||||||
to_str(c_object.plugin_name)
|
to_str(c_object.plugin_name)
|
||||||
.and_then(|n| to_str(c_object.version).map(|v| (n.into(), v.into())))
|
.and_then(|n| to_str(c_object.version).map(|v| (n.into(), v.into())))
|
||||||
|
|||||||
+37
-1
@@ -7,7 +7,7 @@ use loot_condition_interpreter::State;
|
|||||||
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::helpers::{
|
use crate::helpers::{
|
||||||
error, map_game_type, map_plugin_crcs, map_plugin_versions, to_str, to_str_vec,
|
error, map_game_type, map_plugin_crcs, map_plugin_versions, to_path_buf_vec, to_str, to_str_vec,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
@@ -200,3 +200,39 @@ pub unsafe extern "C" fn lci_state_clear_condition_cache(state: *mut lci_state)
|
|||||||
})
|
})
|
||||||
.unwrap_or(LCI_ERROR_PANICKED)
|
.unwrap_or(LCI_ERROR_PANICKED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the external data paths for the given state.
|
||||||
|
///
|
||||||
|
/// If the operating environment contains multiple directories containing relevant plugins and other
|
||||||
|
/// data files, this function can be used to provide those directory paths that are not the game's
|
||||||
|
/// main data directory so that files in those directories are taken into account when evaluating
|
||||||
|
/// conditions.
|
||||||
|
///
|
||||||
|
/// Returns `LCI_OK` if successful, otherwise a `LCI_ERROR_*` code is returned.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn lci_state_set_additional_data_paths(
|
||||||
|
state: *mut lci_state,
|
||||||
|
paths: *const *const c_char,
|
||||||
|
num_paths: size_t,
|
||||||
|
) -> c_int {
|
||||||
|
catch_unwind(|| {
|
||||||
|
if state.is_null() || (paths.is_null() && num_paths != 0) {
|
||||||
|
return error(LCI_ERROR_INVALID_ARGS, "Null pointer passed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = match (*state).0.write() {
|
||||||
|
Err(e) => return error(LCI_ERROR_POISONED_THREAD_LOCK, &e.to_string()),
|
||||||
|
Ok(h) => h,
|
||||||
|
};
|
||||||
|
|
||||||
|
let additional_data_paths = match to_path_buf_vec(paths, num_paths) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(x) => return error(x, "An external data path contained a null byte"),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.set_additional_data_paths(additional_data_paths);
|
||||||
|
|
||||||
|
LCI_OK
|
||||||
|
})
|
||||||
|
.unwrap_or(LCI_ERROR_PANICKED)
|
||||||
|
}
|
||||||
|
|||||||
@@ -181,6 +181,37 @@ void test_lci_state_set_crc_cache() {
|
|||||||
lci_state_destroy(state);
|
lci_state_destroy(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_lci_state_set_additional_data_paths() {
|
||||||
|
printf("testing lci_state_set_additional_data_paths()...\n");
|
||||||
|
|
||||||
|
lci_state * state = nullptr;
|
||||||
|
int return_code = lci_state_create(&state, LCI_GAME_OBLIVION, ".", ".");
|
||||||
|
|
||||||
|
assert(return_code == LCI_OK);
|
||||||
|
assert(state != nullptr);
|
||||||
|
|
||||||
|
return_code = lci_condition_eval("file(\"Blank.esm\")", state);
|
||||||
|
|
||||||
|
assert(return_code == LCI_RESULT_FALSE);
|
||||||
|
|
||||||
|
const char * data_paths[] = { "../../tests/testing-plugins/Oblivion/Data" };
|
||||||
|
|
||||||
|
return_code = lci_state_set_additional_data_paths(state, data_paths, 1);
|
||||||
|
assert(return_code == LCI_OK);
|
||||||
|
|
||||||
|
return_code = lci_state_clear_condition_cache(state);
|
||||||
|
assert(return_code == LCI_OK);
|
||||||
|
|
||||||
|
return_code = lci_condition_eval("file(\"Blank.esm\")", state);
|
||||||
|
|
||||||
|
assert(return_code == LCI_RESULT_TRUE);
|
||||||
|
|
||||||
|
return_code = lci_state_set_additional_data_paths(state, nullptr, 0);
|
||||||
|
assert(return_code == LCI_OK);
|
||||||
|
|
||||||
|
lci_state_destroy(state);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
test_game_id_values();
|
test_game_id_values();
|
||||||
|
|
||||||
@@ -192,6 +223,7 @@ int main(void) {
|
|||||||
test_lci_state_set_active_plugins();
|
test_lci_state_set_active_plugins();
|
||||||
test_lci_state_set_plugin_versions();
|
test_lci_state_set_plugin_versions();
|
||||||
test_lci_state_set_crc_cache();
|
test_lci_state_set_crc_cache();
|
||||||
|
test_lci_state_set_additional_data_paths();
|
||||||
|
|
||||||
printf("SUCCESS\n");
|
printf("SUCCESS\n");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+96
-16
@@ -22,15 +22,21 @@ fn is_match(game_type: GameType, regex: &Regex, file_name: &OsStr) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_file_regex(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
|
fn evaluate_regex(
|
||||||
let dir_iterator = match read_dir(state.data_path.join(parent_path)) {
|
game_type: GameType,
|
||||||
|
data_path: &Path,
|
||||||
|
parent_path: &Path,
|
||||||
|
regex: &Regex,
|
||||||
|
mut condition: impl FnMut() -> bool,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let dir_iterator = match read_dir(data_path.join(parent_path)) {
|
||||||
Ok(i) => i,
|
Ok(i) => i,
|
||||||
Err(_) => return Ok(false),
|
Err(_) => return Ok(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
for entry in dir_iterator {
|
for entry in dir_iterator {
|
||||||
let entry = entry.map_err(|e| Error::IoError(parent_path.to_path_buf(), e))?;
|
let entry = entry.map_err(|e| Error::IoError(parent_path.to_path_buf(), e))?;
|
||||||
if is_match(state.game_type, regex, &entry.file_name()) {
|
if is_match(game_type, regex, &entry.file_name()) && condition() {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,6 +44,24 @@ fn evaluate_file_regex(state: &State, parent_path: &Path, regex: &Regex) -> Resu
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evaluate_file_regex(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
|
||||||
|
for data_path in &state.additional_data_paths {
|
||||||
|
let result = evaluate_regex(state.game_type, data_path, parent_path, regex, || true)?;
|
||||||
|
|
||||||
|
if result {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate_regex(
|
||||||
|
state.game_type,
|
||||||
|
&state.data_path,
|
||||||
|
parent_path,
|
||||||
|
regex,
|
||||||
|
|| true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn evaluate_readable(state: &State, path: &Path) -> Result<bool, Error> {
|
fn evaluate_readable(state: &State, path: &Path) -> Result<bool, Error> {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
Ok(read_dir(resolve_path(state, path)).is_ok())
|
Ok(read_dir(resolve_path(state, path)).is_ok())
|
||||||
@@ -47,24 +71,39 @@ fn evaluate_readable(state: &State, path: &Path) -> Result<bool, Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_many(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
|
fn evaluate_many(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
|
||||||
let dir_iterator = match read_dir(state.data_path.join(parent_path)) {
|
// Share the found_one state across all data paths because they're all
|
||||||
Ok(i) => i,
|
// treated as if they were merged into one directory.
|
||||||
Err(_) => return Ok(false),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut found_one = false;
|
let mut found_one = false;
|
||||||
for entry in dir_iterator {
|
let mut condition = || {
|
||||||
let entry = entry.map_err(|e| Error::IoError(parent_path.to_path_buf(), e))?;
|
|
||||||
if is_match(state.game_type, regex, &entry.file_name()) {
|
|
||||||
if found_one {
|
if found_one {
|
||||||
return Ok(true);
|
true
|
||||||
} else {
|
} else {
|
||||||
found_one = true;
|
found_one = true;
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for data_path in &state.additional_data_paths {
|
||||||
|
let result = evaluate_regex(
|
||||||
|
state.game_type,
|
||||||
|
data_path,
|
||||||
|
parent_path,
|
||||||
|
regex,
|
||||||
|
&mut condition,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if result {
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(false)
|
evaluate_regex(
|
||||||
|
state.game_type,
|
||||||
|
&state.data_path,
|
||||||
|
parent_path,
|
||||||
|
regex,
|
||||||
|
&mut condition,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_active_path(state: &State, path: &Path) -> Result<bool, Error> {
|
fn evaluate_active_path(state: &State, path: &Path) -> Result<bool, Error> {
|
||||||
@@ -290,22 +329,23 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn state_with_active_plugins<T: Into<PathBuf>>(data_path: T, active_plugins: &[&str]) -> State {
|
fn state_with_active_plugins<T: Into<PathBuf>>(data_path: T, active_plugins: &[&str]) -> State {
|
||||||
state_with_data(data_path, "", active_plugins, &[])
|
state_with_data(data_path, Vec::default(), "", active_plugins, &[])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state_with_loot_path<T: Into<PathBuf>>(data_path: T, loot_path: &str) -> State {
|
fn state_with_loot_path<T: Into<PathBuf>>(data_path: T, loot_path: &str) -> State {
|
||||||
state_with_data(data_path, loot_path, &[], &[])
|
state_with_data(data_path, Vec::default(), loot_path, &[], &[])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state_with_versions<T: Into<PathBuf>>(
|
fn state_with_versions<T: Into<PathBuf>>(
|
||||||
data_path: T,
|
data_path: T,
|
||||||
plugin_versions: &[(&str, &str)],
|
plugin_versions: &[(&str, &str)],
|
||||||
) -> State {
|
) -> State {
|
||||||
state_with_data(data_path, "", &[], plugin_versions)
|
state_with_data(data_path, Vec::default(), "", &[], plugin_versions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state_with_data<T: Into<PathBuf>>(
|
fn state_with_data<T: Into<PathBuf>>(
|
||||||
data_path: T,
|
data_path: T,
|
||||||
|
additional_data_paths: Vec<T>,
|
||||||
loot_path: &str,
|
loot_path: &str,
|
||||||
active_plugins: &[&str],
|
active_plugins: &[&str],
|
||||||
plugin_versions: &[(&str, &str)],
|
plugin_versions: &[(&str, &str)],
|
||||||
@@ -315,9 +355,21 @@ mod tests {
|
|||||||
create_dir(&data_path).unwrap();
|
create_dir(&data_path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let additional_data_paths = additional_data_paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|data_path| {
|
||||||
|
let data_path: PathBuf = data_path.into();
|
||||||
|
if !data_path.exists() {
|
||||||
|
create_dir(&data_path).unwrap();
|
||||||
|
}
|
||||||
|
data_path
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
State {
|
State {
|
||||||
game_type: GameType::Oblivion,
|
game_type: GameType::Oblivion,
|
||||||
data_path,
|
data_path,
|
||||||
|
additional_data_paths,
|
||||||
loot_path: loot_path.into(),
|
loot_path: loot_path.into(),
|
||||||
active_plugins: active_plugins
|
active_plugins: active_plugins
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -460,6 +512,20 @@ mod tests {
|
|||||||
assert!(function.eval(&state).unwrap());
|
assert!(function.eval(&state).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_file_regex_eval_should_check_all_configured_data_paths() {
|
||||||
|
let function = Function::FileRegex(PathBuf::from("Data"), regex("Blank\\.esp"));
|
||||||
|
let state = state_with_data(
|
||||||
|
"./src",
|
||||||
|
vec!["./tests/testing-plugins/Oblivion"],
|
||||||
|
".",
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(function.eval(&state).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn function_readable_eval_should_be_true_for_a_file_that_can_be_opened_as_read_only() {
|
fn function_readable_eval_should_be_true_for_a_file_that_can_be_opened_as_read_only() {
|
||||||
let function = Function::Readable(PathBuf::from("Cargo.toml"));
|
let function = Function::Readable(PathBuf::from("Cargo.toml"));
|
||||||
@@ -709,6 +775,20 @@ mod tests {
|
|||||||
assert!(function.eval(&state).unwrap());
|
assert!(function.eval(&state).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_many_eval_should_check_across_all_configured_data_paths() {
|
||||||
|
let function = Function::Many(PathBuf::from("Data"), regex("Blank\\.esp"));
|
||||||
|
let state = state_with_data(
|
||||||
|
"./tests/testing-plugins/Skyrim",
|
||||||
|
vec!["./tests/testing-plugins/Oblivion"],
|
||||||
|
".",
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(function.eval(&state).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn function_many_active_eval_should_be_true_if_the_regex_matches_more_than_one_active_plugin() {
|
fn function_many_active_eval_should_be_true_if_the_regex_matches_more_than_one_active_plugin() {
|
||||||
let function = Function::ManyActive(regex("Blank.*"));
|
let function = Function::ManyActive(regex("Blank.*"));
|
||||||
|
|||||||
@@ -59,6 +59,24 @@ pub fn resolve_path(state: &State, path: &Path) -> PathBuf {
|
|||||||
if path == Path::new("LOOT") {
|
if path == Path::new("LOOT") {
|
||||||
state.loot_path.clone()
|
state.loot_path.clone()
|
||||||
} else {
|
} else {
|
||||||
|
// First check external data paths, as files there may override files in the main data path.
|
||||||
|
for data_path in &state.additional_data_paths {
|
||||||
|
let mut path = data_path.join(path);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_unghosted_plugin_file_extension(state.game_type, &path) {
|
||||||
|
path = add_ghost_extension(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now check the main data path.
|
||||||
let path = state.data_path.join(path);
|
let path = state.data_path.join(path);
|
||||||
|
|
||||||
if !path.exists() && has_unghosted_plugin_file_extension(state.game_type, &path) {
|
if !path.exists() && has_unghosted_plugin_file_extension(state.game_type, &path) {
|
||||||
@@ -435,4 +453,33 @@ mod tests {
|
|||||||
resolved_path
|
resolved_path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_path_should_check_external_data_paths_in_order_before_data_path() {
|
||||||
|
use std::fs::copy;
|
||||||
|
use std::fs::create_dir;
|
||||||
|
|
||||||
|
let tmp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let external_data_path_1 = tmp_dir.path().join("Data1");
|
||||||
|
let external_data_path_2 = tmp_dir.path().join("Data2");
|
||||||
|
let data_path = tmp_dir.path().join("Data3");
|
||||||
|
|
||||||
|
create_dir(&external_data_path_1).unwrap();
|
||||||
|
create_dir(&external_data_path_2).unwrap();
|
||||||
|
create_dir(&data_path).unwrap();
|
||||||
|
copy(
|
||||||
|
Path::new("Cargo.toml"),
|
||||||
|
external_data_path_2.join("Cargo.toml"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
copy(Path::new("Cargo.toml"), data_path.join("Cargo.toml")).unwrap();
|
||||||
|
|
||||||
|
let mut state = State::new(GameType::Skyrim, data_path, "loot.exe".into());
|
||||||
|
state.set_additional_data_paths(vec![external_data_path_1, external_data_path_2.clone()]);
|
||||||
|
|
||||||
|
let input_path = Path::new("Cargo.toml");
|
||||||
|
let resolved_path = resolve_path(&state, input_path);
|
||||||
|
|
||||||
|
assert_eq!(external_data_path_2.join(input_path), resolved_path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ pub struct State {
|
|||||||
game_type: GameType,
|
game_type: GameType,
|
||||||
/// Game Data folder path.
|
/// Game Data folder path.
|
||||||
data_path: PathBuf,
|
data_path: PathBuf,
|
||||||
|
/// Other directories that may contain plugins and other game files, used before data_path and
|
||||||
|
/// in the order they're listed.
|
||||||
|
additional_data_paths: Vec<PathBuf>,
|
||||||
/// Path to the LOOT executable, used to resolve conditions that use the "LOOT" path.
|
/// Path to the LOOT executable, used to resolve conditions that use the "LOOT" path.
|
||||||
loot_path: PathBuf,
|
loot_path: PathBuf,
|
||||||
/// Lowercased plugin filenames.
|
/// Lowercased plugin filenames.
|
||||||
@@ -68,6 +71,7 @@ impl State {
|
|||||||
State {
|
State {
|
||||||
game_type,
|
game_type,
|
||||||
data_path,
|
data_path,
|
||||||
|
additional_data_paths: Vec::default(),
|
||||||
loot_path,
|
loot_path,
|
||||||
active_plugins: HashSet::default(),
|
active_plugins: HashSet::default(),
|
||||||
crc_cache: RwLock::default(),
|
crc_cache: RwLock::default(),
|
||||||
@@ -124,6 +128,10 @@ impl State {
|
|||||||
) -> Result<(), PoisonError<RwLockWriteGuard<HashMap<Function, bool>>>> {
|
) -> Result<(), PoisonError<RwLockWriteGuard<HashMap<Function, bool>>>> {
|
||||||
self.condition_cache.write().map(|mut c| c.clear())
|
self.condition_cache.write().map(|mut c| c.clear())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_additional_data_paths(&mut self, additional_data_paths: Vec<PathBuf>) {
|
||||||
|
self.additional_data_paths = additional_data_paths;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compound conditions joined by 'or'
|
/// Compound conditions joined by 'or'
|
||||||
@@ -287,6 +295,7 @@ mod tests {
|
|||||||
State {
|
State {
|
||||||
game_type: GameType::Oblivion,
|
game_type: GameType::Oblivion,
|
||||||
data_path,
|
data_path,
|
||||||
|
additional_data_paths: Vec::default(),
|
||||||
loot_path: PathBuf::new(),
|
loot_path: PathBuf::new(),
|
||||||
active_plugins: HashSet::new(),
|
active_plugins: HashSet::new(),
|
||||||
crc_cache: RwLock::default(),
|
crc_cache: RwLock::default(),
|
||||||
|
|||||||
Reference in New Issue
Block a user