Expose save-side placed-structure triplets
This commit is contained in:
parent
e01039379b
commit
5928815465
1 changed files with 266 additions and 0 deletions
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue