Bridge packed event collection through save import
This commit is contained in:
parent
6ebe5fffeb
commit
83f55fa26e
13 changed files with 653 additions and 35 deletions
|
|
@ -79,6 +79,13 @@ const RECIPE_BOOK_LINE_STRIDE: usize = 0x30;
|
|||
const RECIPE_BOOK_LINE_AREA_LEN: usize = RECIPE_BOOK_LINE_COUNT * RECIPE_BOOK_LINE_STRIDE;
|
||||
const RECIPE_BOOK_SUMMARY_END_OFFSET: usize =
|
||||
RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_COUNT * RECIPE_BOOK_STRIDE;
|
||||
const EVENT_RUNTIME_COLLECTION_METADATA_TAG: u16 = 0x4e99;
|
||||
const EVENT_RUNTIME_COLLECTION_RECORDS_TAG: u16 = 0x4e9a;
|
||||
const EVENT_RUNTIME_COLLECTION_CLOSE_TAG: u16 = 0x4e9b;
|
||||
const EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION: u32 = 0x000003e9;
|
||||
const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19;
|
||||
const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
|
||||
INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4;
|
||||
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX: usize =
|
||||
(POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - SPECIAL_CONDITIONS_OFFSET) / 4;
|
||||
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT: usize =
|
||||
|
|
@ -1167,6 +1174,23 @@ pub struct SmpLoadedSpecialConditionsTable {
|
|||
pub entries: Vec<SmpSpecialConditionEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpLoadedEventRuntimeCollectionSummary {
|
||||
pub source_kind: String,
|
||||
pub mechanism_family: String,
|
||||
pub mechanism_confidence: String,
|
||||
#[serde(default)]
|
||||
pub container_profile_family: Option<String>,
|
||||
pub metadata_tag_offset: usize,
|
||||
pub records_tag_offset: usize,
|
||||
pub close_tag_offset: usize,
|
||||
pub packed_state_version: u32,
|
||||
pub packed_state_version_hex: String,
|
||||
pub live_id_bound: u32,
|
||||
pub live_record_count: usize,
|
||||
pub live_entry_ids: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpLoadedSaveSlice {
|
||||
pub file_extension_hint: Option<String>,
|
||||
|
|
@ -1178,6 +1202,7 @@ pub struct SmpLoadedSaveSlice {
|
|||
pub profile: Option<SmpLoadedProfile>,
|
||||
pub candidate_availability_table: Option<SmpLoadedCandidateAvailabilityTable>,
|
||||
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
|
||||
pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
@ -1220,6 +1245,7 @@ pub struct SmpInspectionReport {
|
|||
pub classic_rehydrate_profile_probe: Option<SmpClassicRehydrateProfileProbe>,
|
||||
pub rt3_105_packed_profile_probe: Option<SmpRt3105PackedProfileProbe>,
|
||||
pub save_load_summary: Option<SmpSaveLoadSummary>,
|
||||
pub event_runtime_collection_summary: Option<SmpLoadedEventRuntimeCollectionSummary>,
|
||||
pub contains_grounded_runtime_tags: bool,
|
||||
pub known_tag_hits: Vec<SmpKnownTagHit>,
|
||||
pub notes: Vec<String>,
|
||||
|
|
@ -1343,10 +1369,128 @@ pub fn load_save_slice_from_report(
|
|||
profile,
|
||||
candidate_availability_table,
|
||||
special_conditions_table,
|
||||
event_runtime_collection: report.event_runtime_collection_summary.clone(),
|
||||
notes: summary.notes.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_event_runtime_collection_summary(
|
||||
bytes: &[u8],
|
||||
container_profile: Option<&SmpContainerProfile>,
|
||||
save_load_summary: Option<&SmpSaveLoadSummary>,
|
||||
) -> Option<SmpLoadedEventRuntimeCollectionSummary> {
|
||||
let metadata_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG);
|
||||
let record_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG);
|
||||
let close_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG);
|
||||
|
||||
for metadata_tag_offset in metadata_offsets {
|
||||
let packed_state_version = read_u32_at(bytes, metadata_tag_offset + 2)?;
|
||||
if packed_state_version != EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION {
|
||||
continue;
|
||||
}
|
||||
|
||||
let records_tag_offset = record_offsets
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|offset| *offset > metadata_tag_offset + 6)?;
|
||||
let close_tag_offset = close_offsets
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|offset| *offset > records_tag_offset)?;
|
||||
let metadata_payload = bytes.get(metadata_tag_offset + 6..records_tag_offset)?;
|
||||
if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN {
|
||||
continue;
|
||||
}
|
||||
|
||||
let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT)
|
||||
.map(|index| read_u32_at(metadata_payload, index * 4))
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
let direct_collection_flag = header_words[0];
|
||||
let direct_record_stride = usize::try_from(header_words[1]).ok()?;
|
||||
let live_id_bound = header_words[4];
|
||||
let live_record_count = usize::try_from(header_words[5]).ok()?;
|
||||
if direct_collection_flag == 0 || direct_record_stride == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bitset_len = ((usize::try_from(live_id_bound).ok()?).saturating_add(15)) / 8;
|
||||
let payload_bytes = direct_record_stride.checked_mul(live_record_count)?;
|
||||
if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + bitset_len {
|
||||
continue;
|
||||
}
|
||||
if metadata_payload.len() < bitset_len + payload_bytes {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bitset_offset = metadata_payload.len() - bitset_len - payload_bytes;
|
||||
if bitset_offset < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN {
|
||||
continue;
|
||||
}
|
||||
let bitset = metadata_payload.get(bitset_offset..bitset_offset + bitset_len)?;
|
||||
let live_entry_ids = decode_live_entry_ids_from_tombstone_bitset(bitset, live_id_bound)?;
|
||||
if live_entry_ids.len() != live_record_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Some(SmpLoadedEventRuntimeCollectionSummary {
|
||||
source_kind: "packed-event-runtime-collection".to_string(),
|
||||
mechanism_family: save_load_summary
|
||||
.map(|summary| summary.mechanism_family.clone())
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
mechanism_confidence: save_load_summary
|
||||
.map(|summary| summary.mechanism_confidence.clone())
|
||||
.unwrap_or_else(|| "inferred".to_string()),
|
||||
container_profile_family: container_profile
|
||||
.map(|profile| profile.profile_family.clone()),
|
||||
metadata_tag_offset,
|
||||
records_tag_offset,
|
||||
close_tag_offset,
|
||||
packed_state_version,
|
||||
packed_state_version_hex: format!("0x{packed_state_version:08x}"),
|
||||
live_id_bound,
|
||||
live_record_count,
|
||||
live_entry_ids,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn decode_live_entry_ids_from_tombstone_bitset(
|
||||
bitset: &[u8],
|
||||
live_id_bound: u32,
|
||||
) -> Option<Vec<u32>> {
|
||||
let ids = decode_live_entry_ids_with_mapping(bitset, live_id_bound, false);
|
||||
if ids.is_some() {
|
||||
return ids;
|
||||
}
|
||||
decode_live_entry_ids_with_mapping(bitset, live_id_bound, true)
|
||||
}
|
||||
|
||||
fn decode_live_entry_ids_with_mapping(
|
||||
bitset: &[u8],
|
||||
live_id_bound: u32,
|
||||
subtract_one: bool,
|
||||
) -> Option<Vec<u32>> {
|
||||
let mut live_entry_ids = Vec::new();
|
||||
|
||||
for entry_id in 1..=live_id_bound {
|
||||
let bit_index = if subtract_one {
|
||||
entry_id.checked_sub(1)?
|
||||
} else {
|
||||
entry_id
|
||||
};
|
||||
let byte_index = usize::try_from(bit_index / 8).ok()?;
|
||||
let bit_mask = 1u8.checked_shl(bit_index % 8).unwrap_or(0);
|
||||
let tombstone_byte = *bitset.get(byte_index)?;
|
||||
if tombstone_byte & bit_mask == 0 {
|
||||
live_entry_ids.push(entry_id);
|
||||
}
|
||||
}
|
||||
|
||||
Some(live_entry_ids)
|
||||
}
|
||||
|
||||
fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> SmpInspectionReport {
|
||||
let known_tag_hits = KNOWN_TAG_DEFINITIONS
|
||||
.iter()
|
||||
|
|
@ -1475,6 +1619,11 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
|
|||
rt3_105_packed_profile_probe.as_ref(),
|
||||
rt3_105_save_name_table_probe.as_ref(),
|
||||
);
|
||||
let event_runtime_collection_summary = parse_event_runtime_collection_summary(
|
||||
bytes,
|
||||
container_profile.as_ref(),
|
||||
save_load_summary.as_ref(),
|
||||
);
|
||||
let mut warnings = Vec::new();
|
||||
if bytes.is_empty() {
|
||||
warnings
|
||||
|
|
@ -1562,6 +1711,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
|
|||
classic_rehydrate_profile_probe,
|
||||
rt3_105_packed_profile_probe,
|
||||
save_load_summary,
|
||||
event_runtime_collection_summary,
|
||||
contains_grounded_runtime_tags: !known_tag_hits.is_empty(),
|
||||
known_tag_hits,
|
||||
notes: vec![
|
||||
|
|
@ -6025,6 +6175,110 @@ mod tests {
|
|||
assert!(slice.candidate_availability_table.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_event_runtime_collection_summary_from_synthetic_chunks() {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
||||
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
||||
|
||||
let header_words = [1u32, 4, 0, 0, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
for word in header_words {
|
||||
bytes.extend_from_slice(&word.to_le_bytes());
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&[0x14, 0x00]);
|
||||
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
||||
bytes.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]);
|
||||
bytes.extend_from_slice(&[0x55, 0x66, 0x77, 0x88]);
|
||||
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
||||
bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
|
||||
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes());
|
||||
|
||||
let report = inspect_smp_bytes(&bytes);
|
||||
let summary = report
|
||||
.event_runtime_collection_summary
|
||||
.as_ref()
|
||||
.expect("event runtime collection summary should parse");
|
||||
|
||||
assert_eq!(summary.packed_state_version, 0x3e9);
|
||||
assert_eq!(summary.live_id_bound, 5);
|
||||
assert_eq!(summary.live_record_count, 3);
|
||||
assert_eq!(summary.live_entry_ids, vec![1, 3, 5]);
|
||||
assert_eq!(summary.records_tag_offset, 96);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_event_runtime_collection_summary_from_report() {
|
||||
let mut report = inspect_smp_bytes(&[]);
|
||||
let classic_probe = SmpClassicRehydrateProfileProbe {
|
||||
profile_family: "rt3-classic-save-container-v1".to_string(),
|
||||
progress_32dc_offset: 0x76e8,
|
||||
progress_3714_offset: 0x76ec,
|
||||
progress_3715_offset: 0x77f8,
|
||||
packed_profile_offset: 0x76f0,
|
||||
packed_profile_len: 0x108,
|
||||
packed_profile_len_hex: "0x108".to_string(),
|
||||
packed_profile_block: SmpClassicPackedProfileBlock {
|
||||
relative_len: 0x108,
|
||||
relative_len_hex: "0x108".to_string(),
|
||||
leading_word_0: 3,
|
||||
leading_word_0_hex: "0x00000003".to_string(),
|
||||
trailing_zero_word_count_after_leading_word: 3,
|
||||
map_path_offset: 0x13,
|
||||
map_path: Some("British Isles.gmp".to_string()),
|
||||
display_name_offset: 0x46,
|
||||
display_name: Some("British Isles".to_string()),
|
||||
profile_byte_0x77: 0,
|
||||
profile_byte_0x77_hex: "0x00".to_string(),
|
||||
profile_byte_0x82: 0,
|
||||
profile_byte_0x82_hex: "0x00".to_string(),
|
||||
profile_byte_0x97: 0,
|
||||
profile_byte_0x97_hex: "0x00".to_string(),
|
||||
profile_byte_0xc5: 0,
|
||||
profile_byte_0xc5_hex: "0x00".to_string(),
|
||||
stable_nonzero_words: vec![],
|
||||
},
|
||||
ascii_runs: vec![],
|
||||
};
|
||||
report.classic_rehydrate_profile_probe = Some(classic_probe.clone());
|
||||
report.save_load_summary = build_save_load_summary(
|
||||
Some("gms"),
|
||||
Some(&SmpContainerProfile {
|
||||
profile_family: "rt3-classic-save-container-v1".to_string(),
|
||||
profile_evidence: vec![],
|
||||
is_known_profile: true,
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
Some(&classic_probe),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
report.event_runtime_collection_summary = Some(SmpLoadedEventRuntimeCollectionSummary {
|
||||
source_kind: "packed-event-runtime-collection".to_string(),
|
||||
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
||||
mechanism_confidence: "grounded".to_string(),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
metadata_tag_offset: 0x7100,
|
||||
records_tag_offset: 0x7200,
|
||||
close_tag_offset: 0x7600,
|
||||
packed_state_version: 0x3e9,
|
||||
packed_state_version_hex: "0x000003e9".to_string(),
|
||||
live_id_bound: 5,
|
||||
live_record_count: 3,
|
||||
live_entry_ids: vec![1, 3, 5],
|
||||
});
|
||||
|
||||
let slice = load_save_slice_from_report(&report).expect("classic save slice");
|
||||
assert_eq!(
|
||||
slice
|
||||
.event_runtime_collection
|
||||
.as_ref()
|
||||
.map(|summary| summary.live_entry_ids.clone()),
|
||||
Some(vec![1, 3, 5])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_rt3_105_save_slice_from_report() {
|
||||
let mut report = inspect_smp_bytes(&[]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue