Expose save-side placed-structure triplets

This commit is contained in:
Jan Petykiewicz 2026-04-18 08:54:51 -07:00
commit 5928815465

View file

@ -1701,6 +1701,39 @@ pub struct SmpSaveRegionRecordTripletProbe {
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpSavePlacedStructureRecordTripletEntryProbe {
pub record_index: usize,
pub primary_name: String,
pub secondary_name: String,
pub name_tag_relative_offset: usize,
pub policy_tag_relative_offset: usize,
pub profile_tag_relative_offset: usize,
pub policy_chunk_len: usize,
pub profile_chunk_len: usize,
pub policy_f32_lane_0: f32,
pub policy_f32_lane_1: f32,
pub policy_f32_lane_2: f32,
pub policy_f32_lane_3: f32,
pub policy_f32_lane_4: f32,
pub policy_reserved_dword: u32,
pub policy_trailing_word: u16,
pub policy_trailing_word_hex: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpSavePlacedStructureRecordTripletProbe {
pub profile_family: String,
pub source_kind: String,
pub semantic_family: String,
pub records_tag_offset: usize,
pub close_tag_offset: usize,
pub record_count: usize,
#[serde(default)]
pub entries: Vec<SmpSavePlacedStructureRecordTripletEntryProbe>,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpRt3105SaveNameTableProbe {
pub profile_family: String,
@ -2645,6 +2678,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
#[serde(default)]
pub placed_structure_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)]
pub placed_structure_record_triplets: Option<SmpSavePlacedStructureRecordTripletProbe>,
#[serde(default)]
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
#[serde(default)]
pub chairman_entries: Vec<SmpSaveChairmanRecordAnalysisEntry>,
@ -2908,6 +2943,8 @@ pub struct SmpInspectionReport {
pub save_region_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_region_record_triplet_probe: Option<SmpSaveRegionRecordTripletProbe>,
pub save_placed_structure_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_placed_structure_record_triplet_probe:
Option<SmpSavePlacedStructureRecordTripletProbe>,
#[serde(default)]
pub save_company_roster_probe: Option<SmpLoadedCompanyRoster>,
#[serde(default)]
@ -3239,6 +3276,19 @@ pub fn load_save_slice_from_report(
probe.close_tag_offset
));
}
if let Some(probe) = &report.save_placed_structure_record_triplet_probe {
notes.push(format!(
"Raw save tagged placed-structure records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets; first stems={:?}/{:?}, first policy lanes=({:.3}, {:.3}, {:.3}, {:.3}, {:.3}).",
probe.record_count,
probe.entries.first().map(|entry| entry.primary_name.as_str()),
probe.entries.first().map(|entry| entry.secondary_name.as_str()),
probe.entries.first().map(|entry| entry.policy_f32_lane_0).unwrap_or_default(),
probe.entries.first().map(|entry| entry.policy_f32_lane_1).unwrap_or_default(),
probe.entries.first().map(|entry| entry.policy_f32_lane_2).unwrap_or_default(),
probe.entries.first().map(|entry| entry.policy_f32_lane_3).unwrap_or_default(),
probe.entries.first().map(|entry| entry.policy_f32_lane_4).unwrap_or_default()
));
}
if let Some(roster) = &report.save_company_roster_probe {
notes.push(format!(
"Raw save inspection reconstructed {} company direct records from the tagged company collection.",
@ -3301,6 +3351,8 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone();
let train_collection_directory = report.save_train_collection_directory_probe.clone();
let region_record_triplets = report.save_region_record_triplet_probe.clone();
let placed_structure_record_triplets =
report.save_placed_structure_record_triplet_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
@ -3645,6 +3697,14 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
header.live_record_count, header.live_id_bound, header.direct_record_stride
));
}
if let Some(triplets) = placed_structure_record_triplets.as_ref() {
notes.push(format!(
"Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}.",
triplets.record_count,
triplets.entries.first().map(|entry| entry.primary_name.as_str()),
triplets.entries.first().map(|entry| entry.secondary_name.as_str())
));
}
if !company_entries.is_empty() {
notes.push(
"Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string(),
@ -3693,6 +3753,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
placed_structure_collection_header: report
.save_placed_structure_collection_header_probe
.clone(),
placed_structure_record_triplets,
company_entries,
chairman_entries,
notes,
@ -7713,6 +7774,11 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_placed_structure_record_triplet_probe =
parse_save_placed_structure_record_triplet_probe(
bytes,
save_placed_structure_collection_header_probe.as_ref(),
);
let save_company_roster_probe = parse_save_company_roster_probe(
bytes,
save_company_collection_header_probe.as_ref(),
@ -7881,6 +7947,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
save_region_collection_header_probe,
save_region_record_triplet_probe,
save_placed_structure_collection_header_probe,
save_placed_structure_record_triplet_probe,
save_company_roster_probe,
save_chairman_profile_table_probe,
rt3_105_save_name_table_probe,
@ -10060,6 +10127,95 @@ fn parse_save_region_record_triplet_probe(
})
}
fn parse_save_placed_structure_record_triplet_probe(
bytes: &[u8],
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
) -> Option<SmpSavePlacedStructureRecordTripletProbe> {
let header_probe = header_probe?;
if header_probe.source_kind != "save-placed-structure-tagged-header-counts" {
return None;
}
let records_payload =
bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?;
let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG);
let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG);
let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG);
let record_count = header_probe.live_record_count as usize;
if name_offsets.len() != record_count
|| policy_offsets.len() != record_count
|| profile_offsets.len() != record_count
{
return None;
}
let mut entries = Vec::with_capacity(record_count);
for index in 0..record_count {
let name_tag_relative_offset = name_offsets[index];
let policy_tag_relative_offset = policy_offsets[index];
let profile_tag_relative_offset = profile_offsets[index];
let next_record_relative_offset = name_offsets
.get(index + 1)
.copied()
.unwrap_or(records_payload.len());
if !(name_tag_relative_offset < policy_tag_relative_offset
&& policy_tag_relative_offset < profile_tag_relative_offset
&& profile_tag_relative_offset < next_record_relative_offset)
{
return None;
}
let name_payload =
records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?;
let (primary_name, secondary_name) = parse_save_len_prefixed_ascii_name_pair(name_payload)?;
let policy_chunk_len =
profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?;
if policy_chunk_len != 0x1a {
return None;
}
let policy_payload =
records_payload.get(policy_tag_relative_offset + 4..profile_tag_relative_offset)?;
let policy_f32_lane_0 = f32::from_bits(read_u32_at(policy_payload, 0)?);
let policy_f32_lane_1 = f32::from_bits(read_u32_at(policy_payload, 4)?);
let policy_f32_lane_2 = f32::from_bits(read_u32_at(policy_payload, 8)?);
let policy_f32_lane_3 = f32::from_bits(read_u32_at(policy_payload, 12)?);
let policy_f32_lane_4 = f32::from_bits(read_u32_at(policy_payload, 16)?);
let policy_reserved_dword = read_u32_at(policy_payload, 20)?;
let policy_trailing_word = read_u16_at(policy_payload, 24)?;
let profile_chunk_len =
next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?;
entries.push(SmpSavePlacedStructureRecordTripletEntryProbe {
record_index: index,
primary_name,
secondary_name,
name_tag_relative_offset,
policy_tag_relative_offset,
profile_tag_relative_offset,
policy_chunk_len,
profile_chunk_len,
policy_f32_lane_0,
policy_f32_lane_1,
policy_f32_lane_2,
policy_f32_lane_3,
policy_f32_lane_4,
policy_reserved_dword,
policy_trailing_word,
policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"),
});
}
Some(SmpSavePlacedStructureRecordTripletProbe {
profile_family: header_probe.profile_family.clone(),
source_kind: "save-placed-structure-record-triplets".to_string(),
semantic_family: "scenario-save-placed-structure-record-triplets".to_string(),
records_tag_offset: header_probe.records_tag_offset,
close_tag_offset: header_probe.close_tag_offset,
record_count,
entries,
evidence: vec![
"save-side placed-structure records are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the tagged records span".to_string(),
"the 0x55f1 chunk currently exposes two len-prefixed structure-name stems before the fixed 0x55f2 policy row".to_string(),
"each fixed placed-structure 0x55f2 policy chunk currently decodes as five f32-like lanes, one reserved dword, and one trailing u16 word".to_string(),
],
})
}
fn parse_save_placed_structure_collection_header_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
@ -10210,6 +10366,29 @@ fn parse_save_len_prefixed_ascii_name(bytes: &[u8]) -> Option<String> {
Some(text.to_string())
}
fn parse_save_len_prefixed_ascii_name_pair(bytes: &[u8]) -> Option<(String, String)> {
let first_len = *bytes.first()? as usize;
let first_end = 1 + first_len;
let first = std::str::from_utf8(bytes.get(1..first_end)?)
.ok()?
.trim_end_matches('\0')
.to_string();
let mut second_len_offset = first_end;
while matches!(bytes.get(second_len_offset), Some(0)) {
second_len_offset += 1;
}
let second_len = *bytes.get(second_len_offset)? as usize;
let second_start = second_len_offset + 1;
let second = std::str::from_utf8(bytes.get(second_start..second_start + second_len)?)
.ok()?
.trim_end_matches('\0')
.to_string();
if first.is_empty() || second.is_empty() {
return None;
}
Some((first, second))
}
fn parse_save_fixed_ascii_name(bytes: &[u8]) -> Option<String> {
let nul_index = bytes
.iter()
@ -17626,6 +17805,93 @@ mod tests {
assert_eq!(profile_probe.entries[1].trailing_weight_f32, 0.45);
}
#[test]
fn parses_placed_structure_record_triplet_probe_from_dual_name_rows() {
let mut bytes = vec![0u8; 0x400];
let metadata_tag_offset = 0x40usize;
let records_tag_offset = 0x140usize;
let close_tag_offset = 0x260usize;
bytes[metadata_tag_offset..metadata_tag_offset + 4]
.copy_from_slice(&0x000036b1u32.to_le_bytes());
bytes[records_tag_offset..records_tag_offset + 4]
.copy_from_slice(&0x000036b2u32.to_le_bytes());
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000036b3u32.to_le_bytes());
let mut cursor = records_tag_offset + 4;
for (primary, secondary, lane0, lane1, lane2, lane3, lane4) in [
(
"StationA",
"StationSetA",
43111.92f32,
1385.5f32,
34581.95f32,
0.0f32,
5.9760494f32,
),
(
"StationB",
"StationSetB",
44000.0f32,
1200.0f32,
33000.0f32,
0.0f32,
4.5f32,
),
] {
bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes());
bytes[cursor + 4] = primary.len() as u8;
bytes[cursor + 5..cursor + 5 + primary.len()].copy_from_slice(primary.as_bytes());
let second_len_offset = cursor + 5 + primary.len();
bytes[second_len_offset] = secondary.len() as u8;
bytes[second_len_offset + 1..second_len_offset + 1 + secondary.len()]
.copy_from_slice(secondary.as_bytes());
cursor += 0x19;
bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes());
bytes[cursor + 4..cursor + 8].copy_from_slice(&lane0.to_bits().to_le_bytes());
bytes[cursor + 8..cursor + 12].copy_from_slice(&lane1.to_bits().to_le_bytes());
bytes[cursor + 12..cursor + 16].copy_from_slice(&lane2.to_bits().to_le_bytes());
bytes[cursor + 16..cursor + 20].copy_from_slice(&lane3.to_bits().to_le_bytes());
bytes[cursor + 20..cursor + 24].copy_from_slice(&lane4.to_bits().to_le_bytes());
bytes[cursor + 28..cursor + 30].copy_from_slice(&0x0101u16.to_le_bytes());
cursor += 0x1e;
bytes[cursor..cursor + 2]
.copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes());
cursor += 0x10;
}
let header_probe = SmpSaveTaggedCollectionHeaderProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-placed-structure-tagged-header-counts".to_string(),
semantic_family: "scenario-save-placed-structure-header-counts".to_string(),
metadata_tag_offset,
records_tag_offset,
close_tag_offset,
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: 3,
live_id_bound_hex: "0x00000003".to_string(),
live_record_count: 2,
live_record_count_hex: "0x00000002".to_string(),
header_words: vec![0, 6, 0x0a, 0x14, 3, 2],
header_hex_words: vec![],
evidence: vec![],
};
let triplet_probe =
parse_save_placed_structure_record_triplet_probe(&bytes, Some(&header_probe))
.expect("placed-structure triplet probe should parse");
assert_eq!(triplet_probe.record_count, 2);
assert_eq!(triplet_probe.entries[0].primary_name, "StationA");
assert_eq!(triplet_probe.entries[0].secondary_name, "StationSetA");
assert_eq!(triplet_probe.entries[0].policy_chunk_len, 0x1a);
assert_eq!(triplet_probe.entries[0].policy_f32_lane_4, 5.9760494);
assert_eq!(triplet_probe.entries[0].policy_trailing_word, 0x0101);
assert_eq!(triplet_probe.entries[1].primary_name, "StationB");
assert_eq!(triplet_probe.entries[1].secondary_name, "StationSetB");
assert_eq!(triplet_probe.entries[1].policy_f32_lane_1, 1200.0);
}
#[test]
fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() {
let mut bytes = vec![0u8; 0x400];