Add condition field to plugin cleaning data metadata

This commit is contained in:
Oliver Hamlet
2026-01-21 17:55:14 +00:00
parent 027fcd60eb
commit f606f169a5
12 changed files with 446 additions and 151 deletions
@@ -80,7 +80,8 @@ public:
const std::vector<MessageContent>& detail,
unsigned int itm,
unsigned int ref,
unsigned int nav);
unsigned int nav,
std::string_view condition);
/**
* Get the CRC that identifies the plugin that the cleaning data is for.
@@ -122,6 +123,12 @@ public:
*/
LOOT_API std::vector<MessageContent> GetDetail() const;
/**
* Get the condition string.
* @return The cleaning data's condition string.
*/
LOOT_API std::string GetCondition() const;
private:
uint32_t crc_{0};
unsigned int itm_{0};
@@ -129,6 +136,7 @@ private:
unsigned int nav_{0};
std::string utility_;
std::vector<MessageContent> detail_;
std::string condition_;
};
/**
+4 -2
View File
@@ -106,7 +106,8 @@ loot::PluginCleaningData convert(const loot::rust::PluginCleaningData& data) {
convert<loot::MessageContent>(data.detail()),
data.itm_count(),
data.deleted_reference_count(),
data.deleted_navmesh_count());
data.deleted_navmesh_count(),
convert(data.condition()));
}
loot::Location convert(const loot::rust::Location& location) {
@@ -221,7 +222,8 @@ loot::rust::MessageType convert(loot::MessageType messageType) {
convert<loot::rust::MessageContent>(data.GetDetail()),
data.GetITMCount(),
data.GetDeletedReferenceCount(),
data.GetDeletedNavmeshCount());
data.GetDeletedNavmeshCount(),
data.GetCondition());
} catch (const ::rust::Error& e) {
std::rethrow_exception(mapError(e));
}
+15 -2
View File
@@ -34,13 +34,15 @@ PluginCleaningData::PluginCleaningData(
const std::vector<MessageContent>& detail,
unsigned int itm,
unsigned int ref,
unsigned int nav) :
unsigned int nav,
std::string_view condition) :
crc_(crc),
itm_(itm),
ref_(ref),
nav_(nav),
utility_(utility),
detail_(detail) {}
detail_(detail),
condition_(condition) {}
uint32_t PluginCleaningData::GetCRC() const { return crc_; }
@@ -58,12 +60,15 @@ std::vector<MessageContent> PluginCleaningData::GetDetail() const {
return detail_;
}
std::string PluginCleaningData::GetCondition() const { return condition_; }
bool operator==(const PluginCleaningData& lhs, const PluginCleaningData& rhs) {
return lhs.GetCRC() == rhs.GetCRC() &&
lhs.GetITMCount() == rhs.GetITMCount() &&
lhs.GetDeletedReferenceCount() == rhs.GetDeletedReferenceCount() &&
lhs.GetDeletedNavmeshCount() == rhs.GetDeletedNavmeshCount() &&
lhs.GetCleaningUtility() == rhs.GetCleaningUtility() &&
lhs.GetCondition() == rhs.GetCondition() &&
lhs.GetDetail() == rhs.GetDetail();
}
@@ -112,6 +117,14 @@ bool operator<(const PluginCleaningData& lhs, const PluginCleaningData& rhs) {
return false;
}
if (lhs.GetCondition() < rhs.GetCondition()) {
return true;
}
if (rhs.GetCondition() < lhs.GetCondition()) {
return false;
}
return lhs.GetDetail() < rhs.GetDetail();
}
+3
View File
@@ -612,6 +612,7 @@ mod ffi {
itm_count: u32,
deleted_reference_count: u32,
deleted_navmesh_count: u32,
condition: String,
) -> Result<Box<PluginCleaningData>>;
pub fn crc(&self) -> u32;
@@ -626,6 +627,8 @@ mod ffi {
pub fn detail(&self) -> &[MessageContent];
pub fn condition(&self) -> &str;
pub fn boxed_clone(&self) -> Box<PluginCleaningData>;
}
+9
View File
@@ -585,6 +585,7 @@ pub fn new_plugin_cleaning_data(
itm_count: u32,
deleted_reference_count: u32,
deleted_navmesh_count: u32,
condition: String,
) -> Result<Box<PluginCleaningData>, VerboseError> {
let mut data = libloot::metadata::PluginCleaningData::new(crc, cleaning_utility)
.with_itm_count(itm_count)
@@ -595,6 +596,10 @@ pub fn new_plugin_cleaning_data(
data = data.with_detail(to_vec_of_unwrapped(detail))?;
}
if !condition.is_empty() {
data = data.with_condition(condition);
}
Ok(Box::new(PluginCleaningData(data)))
}
@@ -603,6 +608,10 @@ impl PluginCleaningData {
MessageContent::wrap_slice(self.0.detail())
}
pub fn condition(&self) -> &str {
self.0.condition().unwrap_or("")
}
pub fn boxed_clone(&self) -> Box<Self> {
Box::new(Self(self.0.clone()))
}
File diff suppressed because it is too large Load Diff
@@ -205,8 +205,8 @@ TEST_F(PluginMetadataTest, mergeMetadataShouldMergeTags) {
TEST_F(PluginMetadataTest, mergeMetadataShouldMergeDirtyInfoData) {
PluginMetadata plugin1;
PluginMetadata plugin2;
PluginCleaningData info1(0x5, "utility", info_, 1, 2, 3);
PluginCleaningData info2(0xA, "utility", info_, 1, 2, 3);
PluginCleaningData info1(0x5, "utility", info_, 1, 2, 3, "condition");
PluginCleaningData info2(0xA, "utility", info_, 1, 2, 3, "condition");
plugin1.SetDirtyInfo({info1});
plugin2.SetDirtyInfo({info1, info2});
@@ -314,7 +314,8 @@ TEST_F(PluginMetadataTest, hasNameOnlyShouldBeFalseIfTagsExist) {
TEST_F(PluginMetadataTest, hasNameOnlyShouldBeFalseIfDirtyInfoExists) {
PluginMetadata plugin(blankEsp);
plugin.SetDirtyInfo({PluginCleaningData(5, "utility", info_, 0, 1, 2)});
plugin.SetDirtyInfo(
{PluginCleaningData(5, "utility", info_, 0, 1, 2, "condition")});
EXPECT_FALSE(plugin.HasNameOnly());
}
+10
View File
@@ -317,6 +317,7 @@ impl PluginCleaningData {
deleted_reference_count: Option<u32>,
deleted_navmesh_count: Option<u32>,
detail: Option<Vec<&MessageContent>>,
condition: Option<String>,
) -> Result<Self, VerboseError> {
let mut data = libloot::metadata::PluginCleaningData::new(crc, cleaning_utility);
@@ -337,6 +338,10 @@ impl PluginCleaningData {
data = data.with_detail(detail)?;
}
if let Some(condition) = condition {
data = data.with_condition(condition);
}
Ok(Self(data))
}
@@ -369,6 +374,11 @@ impl PluginCleaningData {
pub fn detail(&self) -> Vec<MessageContent> {
self.0.detail().iter().cloned().map(Into::into).collect()
}
#[napi(getter)]
pub fn condition(&self) -> Option<&str> {
self.0.condition()
}
}
impl From<libloot::metadata::PluginCleaningData> for PluginCleaningData {
+14 -3
View File
@@ -391,7 +391,7 @@ pub struct PluginCleaningData(libloot::metadata::PluginCleaningData);
#[pymethods]
impl PluginCleaningData {
#[new]
#[pyo3(signature = (crc, cleaning_utility, itm_count = None, deleted_reference_count = None, deleted_navmesh_count = None, detail = None))]
#[pyo3(signature = (crc, cleaning_utility, itm_count = None, deleted_reference_count = None, deleted_navmesh_count = None, detail = None, condition = None))]
fn new(
crc: u32,
cleaning_utility: String,
@@ -399,6 +399,7 @@ impl PluginCleaningData {
deleted_reference_count: Option<u32>,
deleted_navmesh_count: Option<u32>,
detail: Option<Vec<MessageContent>>,
condition: Option<String>,
) -> Result<Self, VerboseError> {
let mut data = libloot::metadata::PluginCleaningData::new(crc, cleaning_utility);
@@ -419,6 +420,10 @@ impl PluginCleaningData {
data = data.with_detail(detail)?;
}
if let Some(condition) = condition {
data = data.with_condition(condition);
}
Ok(Self(data))
}
@@ -452,18 +457,24 @@ impl PluginCleaningData {
self.0.detail().iter().cloned().map(Into::into).collect()
}
#[getter]
fn condition(&self) -> Option<&str> {
self.0.condition()
}
fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
let class_name = slf.get_type().qualname()?;
let inner = &slf.borrow().0;
Ok(format!(
"{}({}, {}, {}, {}, {}, {})",
"{}({}, {}, {}, {}, {}, {}, {})",
class_name,
inner.crc(),
inner.cleaning_utility(),
inner.itm_count(),
inner.deleted_reference_count(),
inner.deleted_navmesh_count(),
repr_message_contents(inner.detail())
repr_message_contents(inner.detail()),
inner.condition().unwrap_or(NONE_REPR)
))
}
}
+8 -2
View File
@@ -107,7 +107,11 @@ fn filter_cleaning_data_on_conditions(
cleaning_info
.iter()
.filter_map(|i| {
let condition = format!("checksum(\"{}\", {:08X})", plugin_name, i.crc());
let condition = if let Some(c) = i.condition() {
format!("checksum(\"{}\", {:08X}) and ({})", plugin_name, i.crc(), c)
} else {
format!("checksum(\"{}\", {:08X})", plugin_name, i.crc())
};
filter_map_on_condition(i, Some(condition.as_str()), state)
})
@@ -152,7 +156,9 @@ mod tests {
let info1 = PluginCleaningData::new(0x374E_2A6F, "utility1".into());
let info2 = PluginCleaningData::new(0xDEAD_BEEF, "utility2".into());
plugin.set_dirty_info(vec![info1.clone(), info2.clone()]);
let info3 = PluginCleaningData::new(0x374E_2A6F, "utility3".into())
.with_condition(condition.clone());
plugin.set_dirty_info(vec![info1.clone(), info2.clone(), info3.clone()]);
plugin.set_clean_info(vec![info1.clone(), info2.clone()]);
let state = loot_condition_interpreter::State::new(
+23 -2
View File
@@ -632,12 +632,20 @@ fn get_common_metadata_values<'a>(
if !info.detail().is_empty() {
message_contents_counts.increment_for(info.detail());
}
if let Some(condition) = info.condition() {
condition_counts.increment_for(condition);
}
}
for info in plugin.clean_info() {
if !info.detail().is_empty() {
message_contents_counts.increment_for(info.detail());
}
if let Some(condition) = info.condition() {
condition_counts.increment_for(condition);
}
}
}
@@ -1468,6 +1476,7 @@ plugins:
let condition1 = "file(\"test.txt\")";
let condition2 = "file(\"other.txt\")";
let condition3 = "file(\"third.txt\")";
let condition4 = "file(\"fourth.txt\")";
let contents1 = vec![MessageContent::new("message text 1".to_owned())];
let contents2 = vec![
MessageContent::new("message text 1".to_owned()).with_language("en".to_owned()),
@@ -1502,10 +1511,12 @@ plugins:
let info1 = PluginCleaningData::new(0xDEAD_BEEF, "utility".to_owned())
.with_detail(contents2.clone())
.unwrap();
.unwrap()
.with_condition(condition4.to_owned());
let info2 = PluginCleaningData::new(0xDEAD_BEEF, "utility".to_owned())
.with_detail(contents3.clone())
.unwrap();
.unwrap()
.with_condition(condition4.to_owned());
plugin.set_load_after_files(vec![file1.clone()]);
plugin.set_requirements(vec![file1.clone(), file2.clone()]);
@@ -1583,10 +1594,12 @@ plugins:
- crc: 0xDEADBEEF
util: 'utility'
detail: *contents2
condition: &condition4 'file(\"fourth.txt\")'
clean:
- crc: 0xDEADBEEF
util: 'utility'
detail: &contents3 'message text 3'
condition: *condition4
- name: 'test2.esp'
after:
@@ -1641,6 +1654,8 @@ plugins:
- &condition3 'file(\"third.txt\")'
- &condition4 'file(\"fourth.txt\")'
- &message1
type: say
content: *contents2
@@ -1682,10 +1697,12 @@ plugins:
- crc: 0xDEADBEEF
util: 'utility'
detail: *contents2
condition: *condition4
clean:
- crc: 0xDEADBEEF
util: 'utility'
detail: *contents3
condition: *condition4
- name: 'test2.esp'
after:
@@ -1738,6 +1755,8 @@ plugins:
- &condition3 'file(\"third.txt\")'
- &condition4 'file(\"fourth.txt\")'
- &message1
type: say
content: *contents2
@@ -1777,10 +1796,12 @@ plugins:
- crc: 0xDEADBEEF
util: 'utility'
detail: *contents2
condition: *condition4
clean:
- crc: 0xDEADBEEF
util: 'utility'
detail: *contents3
condition: *condition4
- name: 'test2.esp'
after:
+54 -5
View File
@@ -1,5 +1,7 @@
use saphyr::MarkedYaml;
use crate::metadata::yaml::parse_condition;
use super::{
error::{MultilingualMessageContentsError, ParseMetadataError},
message::{
@@ -22,6 +24,7 @@ pub struct PluginCleaningData {
deleted_navmesh_count: u32,
cleaning_utility: Box<str>,
detail: Box<[MessageContent]>,
condition: Option<Box<str>>,
}
impl PluginCleaningData {
@@ -69,6 +72,13 @@ impl PluginCleaningData {
Ok(self)
}
/// Set the condition string.
#[must_use]
pub fn with_condition(mut self, condition: String) -> Self {
self.condition = Some(condition.into_boxed_str());
self
}
/// Get the CRC that identifies the plugin that the cleaning data is for.
pub fn crc(&self) -> u32 {
self.crc
@@ -104,6 +114,11 @@ impl PluginCleaningData {
pub fn detail(&self) -> &[MessageContent] {
&self.detail
}
/// Get the condition string.
pub fn condition(&self) -> Option<&str> {
self.condition.as_deref()
}
}
impl TryFromYaml for PluginCleaningData {
@@ -136,6 +151,8 @@ impl TryFromYaml for PluginCleaningData {
None => Box::default(),
};
let condition = parse_condition(mapping, "condition", YamlObjectType::File)?;
Ok(PluginCleaningData {
crc,
itm_count: itm,
@@ -143,6 +160,7 @@ impl TryFromYaml for PluginCleaningData {
deleted_navmesh_count: nav,
cleaning_utility: util.into(),
detail,
condition,
})
}
}
@@ -177,6 +195,11 @@ impl EmitYaml for PluginCleaningData {
emit_message_contents(&self.detail, emitter);
}
if let Some(condition) = &self.condition {
emitter.write_map_key("condition");
emitter.write_condition(condition);
}
emitter.end_map();
}
}
@@ -250,8 +273,9 @@ mod tests {
#[test]
fn should_set_all_given_fields() {
let yaml =
parse("{crc: 0x12345678, util: cleaner, detail: info, itm: 2, udr: 10, nav: 30}");
let yaml = parse(
"{crc: 0x12345678, util: cleaner, detail: info, itm: 2, udr: 10, nav: 30, condition: file(\"example.esp\")}",
);
let data = PluginCleaningData::try_from_yaml(&yaml).unwrap();
@@ -261,6 +285,7 @@ mod tests {
assert_eq!(2, data.itm_count());
assert_eq!(10, data.deleted_reference_count());
assert_eq!(30, data.deleted_navmesh_count());
assert_eq!("file(\"example.esp\")", data.condition().unwrap());
}
#[test]
@@ -275,6 +300,7 @@ mod tests {
assert_eq!(0, data.itm_count());
assert_eq!(0, data.deleted_reference_count());
assert_eq!(0, data.deleted_navmesh_count());
assert!(data.condition().is_none());
}
#[test]
@@ -400,7 +426,8 @@ detail:
MessageContent::new("english".into()).with_language("en".into()),
MessageContent::new("french".into()).with_language("fr".into()),
])
.unwrap();
.unwrap()
.with_condition("file(\"example.esp\")".into());
let yaml = emit(&data);
assert_eq!(
@@ -414,7 +441,8 @@ detail:
- lang: {}
text: '{}'
- lang: {}
text: '{}'",
text: '{}'
condition: '{}'",
data.cleaning_utility,
data.itm_count,
data.deleted_reference_count,
@@ -422,7 +450,8 @@ detail:
data.detail[0].language(),
data.detail[0].text(),
data.detail[1].language(),
data.detail[1].text()
data.detail[1].text(),
data.condition.unwrap()
),
yaml
);
@@ -445,5 +474,25 @@ detail:
assert_eq!("crc: 0xDEADBEEF\nutil: 'TES5Edit'\ndetail: *content1", yaml);
}
#[test]
fn should_emit_an_alias_if_the_condition_has_an_anchor() {
let data = PluginCleaningData::new(0xDEAD_BEEF, "TES5Edit".into())
.with_condition("file(\"example.esp\")".into());
let mut anchors = YamlAnchors::new();
anchors.set_condition_anchors(HashMap::from([(
data.condition().unwrap(),
"condition1".to_owned(),
)]));
anchors.record_written_anchor("condition1".to_owned());
let yaml = emit_with_anchors(&data, anchors);
assert_eq!(
"crc: 0xDEADBEEF\nutil: 'TES5Edit'\ncondition: *condition1",
yaml
);
}
}
}