Correct save train and region collection probes

This commit is contained in:
Jan Petykiewicz 2026-04-18 08:26:58 -07:00
commit 86511f9670
5 changed files with 251 additions and 113 deletions

View file

@ -1614,7 +1614,7 @@ pub struct SmpSaveTaggedCollectionHeaderProbe {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveRegionCollectionDirectoryEntryProbe {
pub struct SmpSaveTrainCollectionDirectoryEntryProbe {
pub live_entry_id: u32,
pub payload_relative_offset: u32,
pub payload_relative_offset_hex: String,
@ -1626,7 +1626,7 @@ pub struct SmpSaveRegionCollectionDirectoryEntryProbe {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveRegionCollectionDirectoryProbe {
pub struct SmpSaveTrainCollectionDirectoryProbe {
pub profile_family: String,
pub source_kind: String,
pub semantic_family: String,
@ -1641,7 +1641,7 @@ pub struct SmpSaveRegionCollectionDirectoryProbe {
pub chain_head_live_entry_id: Option<u32>,
#[serde(default)]
pub chain_tail_live_entry_id: Option<u32>,
pub entries: Vec<SmpSaveRegionCollectionDirectoryEntryProbe>,
pub entries: Vec<SmpSaveTrainCollectionDirectoryEntryProbe>,
pub evidence: Vec<String>,
}
@ -2579,9 +2579,11 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
#[serde(default)]
pub world_finance_neighborhood: Option<SmpSaveWorldFinanceNeighborhoodProbe>,
#[serde(default)]
pub region_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub train_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)]
pub region_collection_directory: Option<SmpSaveRegionCollectionDirectoryProbe>,
pub train_collection_directory: Option<SmpSaveTrainCollectionDirectoryProbe>,
#[serde(default)]
pub region_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)]
pub placed_structure_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)]
@ -2843,8 +2845,9 @@ pub struct SmpInspectionReport {
pub save_world_finance_neighborhood_probe: Option<SmpSaveWorldFinanceNeighborhoodProbe>,
pub save_company_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_chairman_profile_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_train_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_train_collection_directory_probe: Option<SmpSaveTrainCollectionDirectoryProbe>,
pub save_region_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_region_collection_directory_probe: Option<SmpSaveRegionCollectionDirectoryProbe>,
pub save_placed_structure_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)]
pub save_company_roster_probe: Option<SmpLoadedCompanyRoster>,
@ -3109,9 +3112,9 @@ pub fn load_save_slice_from_report(
probe.close_tag_offset
));
}
if let Some(probe) = &report.save_region_collection_header_probe {
if let Some(probe) = &report.save_train_collection_header_probe {
notes.push(format!(
"Raw save tagged region header reports live_record_count={} and live_id_bound={} with direct_record_stride=0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
"Raw save tagged train header reports live_record_count={} and live_id_bound={} with direct_record_stride=0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
probe.live_record_count,
probe.live_id_bound,
probe.direct_record_stride,
@ -3120,15 +3123,26 @@ pub fn load_save_slice_from_report(
probe.close_tag_offset
));
}
if let Some(probe) = &report.save_region_collection_directory_probe {
if let Some(probe) = &report.save_train_collection_directory_probe {
notes.push(format!(
"Raw save tagged region metadata also exposes a live-entry directory with {} entries rooted at metadata dword {} (head={:?}, tail={:?}).",
"Raw save tagged train metadata also exposes a live-entry directory with {} entries rooted at metadata dword {} (head={:?}, tail={:?}).",
probe.entries.len(),
probe.directory_root_dword_index,
probe.chain_head_live_entry_id,
probe.chain_tail_live_entry_id
));
}
if let Some(probe) = &report.save_region_collection_header_probe {
notes.push(format!(
"Raw save tagged region header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
probe.live_record_count,
probe.live_id_bound,
probe.direct_record_stride,
probe.metadata_tag_offset,
probe.records_tag_offset,
probe.close_tag_offset
));
}
if let Some(probe) = &report.save_placed_structure_collection_header_probe {
notes.push(format!(
"Raw save tagged placed-structure header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
@ -3200,7 +3214,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
let world_issue_37 = report.save_world_issue_37_probe.clone();
let world_economic_tuning = report.save_world_economic_tuning_probe.clone();
let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone();
let region_collection_directory = report.save_region_collection_directory_probe.clone();
let train_collection_directory = report.save_train_collection_directory_probe.clone();
let company_header_probe = report.save_company_collection_header_probe.as_ref();
let chairman_header_probe = report
.save_chairman_profile_collection_header_probe
@ -3490,21 +3504,27 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
.to_string(),
);
}
if let Some(header) = report.save_region_collection_header_probe.as_ref() {
if let Some(header) = report.save_train_collection_header_probe.as_ref() {
notes.push(format!(
"Region analysis now also exports the tagged region collection header: live_record_count={} live_id_bound={} direct_record_stride=0x{:x}.",
"Train analysis now also exports the tagged train collection header: live_record_count={} live_id_bound={} direct_record_stride=0x{:x}.",
header.live_record_count, header.live_id_bound, header.direct_record_stride
));
}
if let Some(directory) = region_collection_directory.as_ref() {
if let Some(directory) = train_collection_directory.as_ref() {
notes.push(format!(
"Region analysis now also exports the tagged live-entry directory rooted at metadata dword {}: {} entries chained from {:?} to {:?}.",
"Train analysis now also exports the tagged live-entry directory rooted at metadata dword {}: {} entries chained from {:?} to {:?}.",
directory.directory_root_dword_index,
directory.entries.len(),
directory.chain_head_live_entry_id,
directory.chain_tail_live_entry_id
));
}
if let Some(header) = report.save_region_collection_header_probe.as_ref() {
notes.push(format!(
"Region analysis now also exports the non-direct tagged region collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.",
header.live_record_count, header.live_id_bound, header.direct_record_stride
));
}
if let Some(header) = report
.save_placed_structure_collection_header_probe
.as_ref()
@ -3555,8 +3575,9 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
world_issue_37,
world_economic_tuning,
world_finance_neighborhood,
train_collection_header: report.save_train_collection_header_probe.clone(),
train_collection_directory,
region_collection_header: report.save_region_collection_header_probe.clone(),
region_collection_directory,
placed_structure_collection_header: report
.save_placed_structure_collection_header_probe
.clone(),
@ -7558,14 +7579,19 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_region_collection_header_probe = parse_save_region_collection_header_probe(
let save_train_collection_header_probe = parse_save_train_collection_header_probe(
bytes,
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_region_collection_directory_probe = parse_save_region_collection_directory_probe(
let save_train_collection_directory_probe = parse_save_train_collection_directory_probe(
bytes,
save_region_collection_header_probe.as_ref(),
save_train_collection_header_probe.as_ref(),
);
let save_region_collection_header_probe = parse_save_region_collection_header_probe(
bytes,
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_placed_structure_collection_header_probe =
parse_save_placed_structure_collection_header_probe(
@ -7736,8 +7762,9 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
save_world_finance_neighborhood_probe,
save_company_collection_header_probe,
save_chairman_profile_collection_header_probe,
save_train_collection_header_probe,
save_train_collection_directory_probe,
save_region_collection_header_probe,
save_region_collection_directory_probe,
save_placed_structure_collection_header_probe,
save_company_roster_probe,
save_chairman_profile_table_probe,
@ -9677,7 +9704,7 @@ fn parse_save_chairman_profile_collection_header_probe(
)
}
fn parse_save_region_collection_header_probe(
fn parse_save_train_collection_header_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
container_profile: Option<&SmpContainerProfile>,
@ -9689,8 +9716,8 @@ fn parse_save_region_collection_header_probe(
0x00005209,
0x0000520a,
0x0000520b,
"save-region-tagged-header-counts",
"scenario-save-region-header-counts",
"save-train-tagged-header-counts",
"scenario-save-train-header-counts",
|header| {
header.direct_collection_flag == 1
&& header.direct_record_stride >= 0x100
@ -9701,18 +9728,18 @@ fn parse_save_region_collection_header_probe(
&& header.live_record_count <= header.live_id_bound
},
vec![
"save-side live region collection shares tagged header family 0x5209/0x520a/0x520b with other indexed direct-record bundles".to_string(),
"the grounded region-side candidate is the smaller direct-record family with stride 0x1d5 and live_id_bound/count in the city-region range, not the larger chairman/profile family".to_string(),
"save-side live train collection shares tagged header family 0x5209/0x520a/0x520b with other indexed direct-record bundles".to_string(),
"the grounded train-side candidate is the smaller direct-record family with stride 0x1d5 whose metadata payload carries Train N labels, distinct from the larger chairman/profile family and the non-direct region family".to_string(),
],
)
}
fn parse_save_region_collection_directory_probe(
fn parse_save_train_collection_directory_probe(
bytes: &[u8],
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
) -> Option<SmpSaveRegionCollectionDirectoryProbe> {
) -> Option<SmpSaveTrainCollectionDirectoryProbe> {
let header_probe = header_probe?;
if header_probe.source_kind != "save-region-tagged-header-counts" {
if header_probe.source_kind != "save-train-tagged-header-counts" {
return None;
}
let metadata_payload =
@ -9733,7 +9760,7 @@ fn parse_save_region_collection_directory_probe(
let payload_relative_offset = read_u32_at(directory_bytes, entry_offset)?;
let previous_live_entry_id = read_u32_at(directory_bytes, entry_offset + 4)?;
let next_live_entry_id = read_u32_at(directory_bytes, entry_offset + 8)?;
entries.push(SmpSaveRegionCollectionDirectoryEntryProbe {
entries.push(SmpSaveTrainCollectionDirectoryEntryProbe {
live_entry_id: (index + 1) as u32,
payload_relative_offset,
payload_relative_offset_hex: format!("0x{payload_relative_offset:08x}"),
@ -9758,10 +9785,10 @@ fn parse_save_region_collection_directory_probe(
let monotonic_offsets = entries
.windows(2)
.all(|window| window[0].payload_relative_offset < window[1].payload_relative_offset);
Some(SmpSaveRegionCollectionDirectoryProbe {
Some(SmpSaveTrainCollectionDirectoryProbe {
profile_family: header_probe.profile_family.clone(),
source_kind: "save-region-live-directory".to_string(),
semantic_family: "scenario-save-region-live-directory".to_string(),
source_kind: "save-train-live-directory".to_string(),
semantic_family: "scenario-save-train-live-directory".to_string(),
metadata_tag_offset: header_probe.metadata_tag_offset,
records_tag_offset: header_probe.records_tag_offset,
close_tag_offset: header_probe.close_tag_offset,
@ -9773,9 +9800,9 @@ fn parse_save_region_collection_directory_probe(
chain_tail_live_entry_id,
entries,
evidence: vec![
"save-side region metadata payload exposes a live-entry directory immediately after the first 16 dwords, before the records tag".to_string(),
"save-side train metadata payload exposes a live-entry directory immediately after the first 16 dwords, with payload-relative offsets pointing into the later records span".to_string(),
format!(
"region live directory decodes {} triplets of (payload_relative_offset, prev_live_entry_id, next_live_entry_id) from metadata dword index {}",
"train live directory decodes {} triplets of (payload_relative_offset, prev_live_entry_id, next_live_entry_id) from metadata dword index {}",
header_probe.live_record_count,
SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX
),
@ -9787,6 +9814,42 @@ fn parse_save_region_collection_directory_probe(
})
}
fn parse_save_region_collection_header_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
container_profile: Option<&SmpContainerProfile>,
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
let probe = parse_save_tagged_collection_header_probe(
bytes,
file_extension_hint,
container_profile,
0x00005209,
0x0000520a,
0x0000520b,
"save-region-tagged-header-counts",
"scenario-save-region-header-counts",
|header| {
header.direct_collection_flag == 0
&& header.direct_record_stride == 0x06
&& header.live_id_bound >= 0x80
&& header.live_id_bound <= 0x200
&& header.live_record_count >= 0x80
&& header.live_record_count <= header.live_id_bound
},
vec![
"save-side live region collection shares tagged header family 0x5209/0x520a/0x520b with trains and chairman profiles, but uses the larger non-direct indexed family".to_string(),
"the grounded region-side candidate is the non-direct 0x5209 family with live_id_bound/count in the 0x96/0x91 range and Marker09-style default stems in the records span, distinct from the smaller direct train family".to_string(),
],
)?;
let records_preview = bytes
.get(probe.records_tag_offset + 4..probe.close_tag_offset)
.unwrap_or(&[]);
records_preview
.windows("Marker09".len())
.any(|window| window == b"Marker09")
.then_some(probe)
}
fn parse_save_placed_structure_collection_header_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
@ -16657,10 +16720,10 @@ mod tests {
],
evidence: vec![],
});
report.save_region_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe {
report.save_train_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-region-tagged-header-counts".to_string(),
semantic_family: "scenario-save-region-header-counts".to_string(),
source_kind: "save-train-tagged-header-counts".to_string(),
semantic_family: "scenario-save-train-header-counts".to_string(),
metadata_tag_offset: 0x3000,
records_tag_offset: 0x3100,
close_tag_offset: 0x3200,
@ -16683,44 +16746,69 @@ mod tests {
],
evidence: vec![],
});
report.save_region_collection_directory_probe =
Some(SmpSaveRegionCollectionDirectoryProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-region-live-directory".to_string(),
semantic_family: "scenario-save-region-live-directory".to_string(),
metadata_tag_offset: 0x3000,
records_tag_offset: 0x3100,
close_tag_offset: 0x3200,
directory_root_dword_index: 16,
directory_entry_dword_count: 3,
live_record_count: 0x14,
live_id_bound: 0x32,
chain_head_live_entry_id: Some(1),
chain_tail_live_entry_id: Some(20),
entries: vec![
SmpSaveRegionCollectionDirectoryEntryProbe {
live_entry_id: 1,
payload_relative_offset: 0x2af8,
payload_relative_offset_hex: "0x00002af8".to_string(),
payload_absolute_offset: 0x5afc,
previous_live_entry_id: 0,
previous_live_entry_id_hex: "0x00000000".to_string(),
next_live_entry_id: 2,
next_live_entry_id_hex: "0x00000002".to_string(),
},
SmpSaveRegionCollectionDirectoryEntryProbe {
live_entry_id: 2,
payload_relative_offset: 0x2ee0,
payload_relative_offset_hex: "0x00002ee0".to_string(),
payload_absolute_offset: 0x5ee4,
previous_live_entry_id: 1,
previous_live_entry_id_hex: "0x00000001".to_string(),
next_live_entry_id: 0,
next_live_entry_id_hex: "0x00000000".to_string(),
},
],
evidence: vec![],
});
report.save_train_collection_directory_probe = Some(SmpSaveTrainCollectionDirectoryProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-train-live-directory".to_string(),
semantic_family: "scenario-save-train-live-directory".to_string(),
metadata_tag_offset: 0x3000,
records_tag_offset: 0x3100,
close_tag_offset: 0x3200,
directory_root_dword_index: 16,
directory_entry_dword_count: 3,
live_record_count: 0x14,
live_id_bound: 0x32,
chain_head_live_entry_id: Some(1),
chain_tail_live_entry_id: Some(20),
entries: vec![
SmpSaveTrainCollectionDirectoryEntryProbe {
live_entry_id: 1,
payload_relative_offset: 0x2af8,
payload_relative_offset_hex: "0x00002af8".to_string(),
payload_absolute_offset: 0x5afc,
previous_live_entry_id: 0,
previous_live_entry_id_hex: "0x00000000".to_string(),
next_live_entry_id: 2,
next_live_entry_id_hex: "0x00000002".to_string(),
},
SmpSaveTrainCollectionDirectoryEntryProbe {
live_entry_id: 2,
payload_relative_offset: 0x2ee0,
payload_relative_offset_hex: "0x00002ee0".to_string(),
payload_absolute_offset: 0x5ee4,
previous_live_entry_id: 1,
previous_live_entry_id_hex: "0x00000001".to_string(),
next_live_entry_id: 0,
next_live_entry_id_hex: "0x00000000".to_string(),
},
],
evidence: vec![],
});
report.save_region_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-region-tagged-header-counts".to_string(),
semantic_family: "scenario-save-region-header-counts".to_string(),
metadata_tag_offset: 0x5000,
records_tag_offset: 0x5100,
close_tag_offset: 0x5200,
direct_collection_flag: 0,
direct_collection_flag_hex: "0x00000000".to_string(),
direct_record_stride: 0x06,
direct_record_stride_hex: "0x00000006".to_string(),
live_id_bound: 0x96,
live_id_bound_hex: "0x00000096".to_string(),
live_record_count: 0x91,
live_record_count_hex: "0x00000091".to_string(),
header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x91],
header_hex_words: vec![
"0x00000000".to_string(),
"0x00000006".to_string(),
"0x0000000a".to_string(),
"0x00000014".to_string(),
"0x00000096".to_string(),
"0x00000091".to_string(),
],
evidence: vec![],
});
report.save_placed_structure_collection_header_probe =
Some(SmpSaveTaggedCollectionHeaderProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
@ -16818,11 +16906,16 @@ mod tests {
slice
.notes
.iter()
.any(|note| note.contains("tagged region header reports live_record_count=20"))
.any(|note| note.contains("tagged train header reports live_record_count=20"))
);
assert!(slice.notes.iter().any(|note| {
note.contains("tagged region metadata also exposes a live-entry directory")
note.contains("tagged train metadata also exposes a live-entry directory")
}));
assert!(
slice.notes.iter().any(|note| {
note.contains("tagged region header reports live_record_count=145")
})
);
assert!(slice.notes.iter().any(|note| {
note.contains("tagged placed-structure header reports live_record_count=2026")
}));
@ -16905,7 +16998,7 @@ mod tests {
}
#[test]
fn parses_region_tagged_collection_header_probe_from_exact_u32_tags() {
fn parses_train_tagged_collection_header_probe_from_exact_u32_tags() {
let mut bytes = vec![0u8; 0x400];
let metadata_tag_offset = 0x40usize;
let records_tag_offset = 0x140usize;
@ -16923,6 +17016,47 @@ mod tests {
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
let probe = parse_save_train_collection_header_probe(
&bytes,
Some("gms"),
Some(&SmpContainerProfile {
profile_family: "rt3-105-save-container-v1".to_string(),
profile_evidence: vec![],
is_known_profile: true,
}),
)
.expect("train header probe should parse");
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
assert_eq!(probe.records_tag_offset, records_tag_offset);
assert_eq!(probe.close_tag_offset, close_tag_offset);
assert_eq!(probe.direct_collection_flag, 1);
assert_eq!(probe.direct_record_stride, 0x1d5);
assert_eq!(probe.live_id_bound, 0x32);
assert_eq!(probe.live_record_count, 0x14);
}
#[test]
fn parses_region_tagged_collection_header_probe_from_marker09_family() {
let mut bytes = vec![0u8; 0x400];
let metadata_tag_offset = 0x40usize;
let records_tag_offset = 0x180usize;
let close_tag_offset = 0x1c0usize;
bytes[metadata_tag_offset..metadata_tag_offset + 4]
.copy_from_slice(&0x00005209u32.to_le_bytes());
bytes[records_tag_offset..records_tag_offset + 4]
.copy_from_slice(&0x0000520au32.to_le_bytes());
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
let header_words = [
0u32, 0x06, 0x0a, 0x14, 0x96, 0x91, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
];
for (index, word) in header_words.into_iter().enumerate() {
let offset = metadata_tag_offset + 4 + index * 4;
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
let marker_offset = records_tag_offset + 4 + 0x20;
bytes[marker_offset..marker_offset + 8].copy_from_slice(b"Marker09");
let probe = parse_save_region_collection_header_probe(
&bytes,
Some("gms"),
@ -16937,14 +17071,14 @@ mod tests {
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
assert_eq!(probe.records_tag_offset, records_tag_offset);
assert_eq!(probe.close_tag_offset, close_tag_offset);
assert_eq!(probe.direct_collection_flag, 1);
assert_eq!(probe.direct_record_stride, 0x1d5);
assert_eq!(probe.live_id_bound, 0x32);
assert_eq!(probe.live_record_count, 0x14);
assert_eq!(probe.direct_collection_flag, 0);
assert_eq!(probe.direct_record_stride, 0x06);
assert_eq!(probe.live_id_bound, 0x96);
assert_eq!(probe.live_record_count, 0x91);
}
#[test]
fn parses_region_collection_directory_probe_from_tagged_metadata_triplets() {
fn parses_train_collection_directory_probe_from_tagged_metadata_triplets() {
let mut bytes = vec![0u8; 0x400];
let metadata_tag_offset = 0x40usize;
let records_tag_offset = 0x180usize;
@ -16971,7 +17105,7 @@ mod tests {
bytes[base + 8..base + 12].copy_from_slice(&next.to_le_bytes());
}
let header_probe = parse_save_region_collection_header_probe(
let header_probe = parse_save_train_collection_header_probe(
&bytes,
Some("gms"),
Some(&SmpContainerProfile {
@ -16980,10 +17114,10 @@ mod tests {
is_known_profile: true,
}),
)
.expect("region header probe should parse");
.expect("train header probe should parse");
let directory_probe =
parse_save_region_collection_directory_probe(&bytes, Some(&header_probe))
.expect("region directory probe should parse");
parse_save_train_collection_directory_probe(&bytes, Some(&header_probe))
.expect("train directory probe should parse");
assert_eq!(directory_probe.directory_root_dword_index, 16);
assert_eq!(directory_probe.live_record_count, 3);