Expose save-side region record triplets
This commit is contained in:
parent
86511f9670
commit
8861074c1b
2 changed files with 222 additions and 4 deletions
|
|
@ -152,6 +152,9 @@ const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
|
|||
INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4;
|
||||
const SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX: usize = 16;
|
||||
const SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT: usize = 3;
|
||||
const SAVE_REGION_RECORD_NAME_TAG: u16 = 0x55f1;
|
||||
const SAVE_REGION_RECORD_POLICY_TAG: u16 = 0x55f2;
|
||||
const SAVE_REGION_RECORD_PROFILE_TAG: u16 = 0x55f3;
|
||||
const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001";
|
||||
const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1";
|
||||
const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1";
|
||||
|
|
@ -1645,6 +1648,30 @@ pub struct SmpSaveTrainCollectionDirectoryProbe {
|
|||
pub evidence: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpSaveRegionRecordTripletEntryProbe {
|
||||
pub record_index: usize,
|
||||
pub 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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpSaveRegionRecordTripletProbe {
|
||||
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<SmpSaveRegionRecordTripletEntryProbe>,
|
||||
pub evidence: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SmpRt3105SaveNameTableProbe {
|
||||
pub profile_family: String,
|
||||
|
|
@ -2585,6 +2612,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
|
|||
#[serde(default)]
|
||||
pub region_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
||||
#[serde(default)]
|
||||
pub region_record_triplets: Option<SmpSaveRegionRecordTripletProbe>,
|
||||
#[serde(default)]
|
||||
pub placed_structure_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
||||
#[serde(default)]
|
||||
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
|
||||
|
|
@ -2848,6 +2877,7 @@ pub struct SmpInspectionReport {
|
|||
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_record_triplet_probe: Option<SmpSaveRegionRecordTripletProbe>,
|
||||
pub save_placed_structure_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
||||
#[serde(default)]
|
||||
pub save_company_roster_probe: Option<SmpLoadedCompanyRoster>,
|
||||
|
|
@ -3143,6 +3173,13 @@ pub fn load_save_slice_from_report(
|
|||
probe.close_tag_offset
|
||||
));
|
||||
}
|
||||
if let Some(probe) = &report.save_region_record_triplet_probe {
|
||||
notes.push(format!(
|
||||
"Raw save tagged region records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets in the records span; first name={:?}.",
|
||||
probe.record_count,
|
||||
probe.entries.first().map(|entry| entry.name.as_str())
|
||||
));
|
||||
}
|
||||
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}.",
|
||||
|
|
@ -3215,6 +3252,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
let world_economic_tuning = report.save_world_economic_tuning_probe.clone();
|
||||
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 company_header_probe = report.save_company_collection_header_probe.as_ref();
|
||||
let chairman_header_probe = report
|
||||
.save_chairman_profile_collection_header_probe
|
||||
|
|
@ -3525,6 +3563,13 @@ 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) = region_record_triplets.as_ref() {
|
||||
notes.push(format!(
|
||||
"Region analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first serialized region name={:?}.",
|
||||
triplets.record_count,
|
||||
triplets.entries.first().map(|entry| entry.name.as_str())
|
||||
));
|
||||
}
|
||||
if let Some(header) = report
|
||||
.save_placed_structure_collection_header_probe
|
||||
.as_ref()
|
||||
|
|
@ -3578,6 +3623,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|||
train_collection_header: report.save_train_collection_header_probe.clone(),
|
||||
train_collection_directory,
|
||||
region_collection_header: report.save_region_collection_header_probe.clone(),
|
||||
region_record_triplets,
|
||||
placed_structure_collection_header: report
|
||||
.save_placed_structure_collection_header_probe
|
||||
.clone(),
|
||||
|
|
@ -7593,6 +7639,8 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
|
|||
file_extension_hint.as_deref(),
|
||||
container_profile.as_ref(),
|
||||
);
|
||||
let save_region_record_triplet_probe =
|
||||
parse_save_region_record_triplet_probe(bytes, save_region_collection_header_probe.as_ref());
|
||||
let save_placed_structure_collection_header_probe =
|
||||
parse_save_placed_structure_collection_header_probe(
|
||||
bytes,
|
||||
|
|
@ -7765,6 +7813,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
|
|||
save_train_collection_header_probe,
|
||||
save_train_collection_directory_probe,
|
||||
save_region_collection_header_probe,
|
||||
save_region_record_triplet_probe,
|
||||
save_placed_structure_collection_header_probe,
|
||||
save_company_roster_probe,
|
||||
save_chairman_profile_table_probe,
|
||||
|
|
@ -9850,6 +9899,76 @@ fn parse_save_region_collection_header_probe(
|
|||
.then_some(probe)
|
||||
}
|
||||
|
||||
fn parse_save_region_record_triplet_probe(
|
||||
bytes: &[u8],
|
||||
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
||||
) -> Option<SmpSaveRegionRecordTripletProbe> {
|
||||
let header_probe = header_probe?;
|
||||
if header_probe.source_kind != "save-region-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 name = parse_save_len_prefixed_ascii_name(name_payload)?;
|
||||
let policy_chunk_len =
|
||||
profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?;
|
||||
let profile_chunk_len =
|
||||
next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?;
|
||||
entries.push(SmpSaveRegionRecordTripletEntryProbe {
|
||||
record_index: index,
|
||||
name,
|
||||
name_tag_relative_offset,
|
||||
policy_tag_relative_offset,
|
||||
profile_tag_relative_offset,
|
||||
policy_chunk_len,
|
||||
profile_chunk_len,
|
||||
});
|
||||
}
|
||||
Some(SmpSaveRegionRecordTripletProbe {
|
||||
profile_family: header_probe.profile_family.clone(),
|
||||
source_kind: "save-region-record-triplets".to_string(),
|
||||
semantic_family: "scenario-save-region-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 region records in the non-direct Marker09 family are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the records span".to_string(),
|
||||
format!(
|
||||
"decoded {} region record triplets with one len-prefixed name chunk, one fixed policy chunk, and one trailing profile payload chunk per record",
|
||||
record_count
|
||||
),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_save_placed_structure_collection_header_probe(
|
||||
bytes: &[u8],
|
||||
file_extension_hint: Option<&str>,
|
||||
|
|
@ -9993,6 +10112,13 @@ fn parse_save_tagged_collection_header_probe(
|
|||
})
|
||||
}
|
||||
|
||||
fn parse_save_len_prefixed_ascii_name(bytes: &[u8]) -> Option<String> {
|
||||
let len = *bytes.first()? as usize;
|
||||
let text_bytes = bytes.get(1..1 + len)?;
|
||||
let text = std::str::from_utf8(text_bytes).ok()?.trim_end_matches('\0');
|
||||
Some(text.to_string())
|
||||
}
|
||||
|
||||
fn parse_rt3_105_save_name_table_probe(
|
||||
bytes: &[u8],
|
||||
file_extension_hint: Option<&str>,
|
||||
|
|
@ -16809,6 +16935,35 @@ mod tests {
|
|||
],
|
||||
evidence: vec![],
|
||||
});
|
||||
report.save_region_record_triplet_probe = Some(SmpSaveRegionRecordTripletProbe {
|
||||
profile_family: "rt3-105-save-container-v1".to_string(),
|
||||
source_kind: "save-region-record-triplets".to_string(),
|
||||
semantic_family: "scenario-save-region-record-triplets".to_string(),
|
||||
records_tag_offset: 0x5100,
|
||||
close_tag_offset: 0x5200,
|
||||
record_count: 2,
|
||||
entries: vec![
|
||||
SmpSaveRegionRecordTripletEntryProbe {
|
||||
record_index: 0,
|
||||
name: "Marker09".to_string(),
|
||||
name_tag_relative_offset: 0,
|
||||
policy_tag_relative_offset: 0x10,
|
||||
profile_tag_relative_offset: 0x2e,
|
||||
policy_chunk_len: 0x1a,
|
||||
profile_chunk_len: 0x40,
|
||||
},
|
||||
SmpSaveRegionRecordTripletEntryProbe {
|
||||
record_index: 1,
|
||||
name: "Marker10".to_string(),
|
||||
name_tag_relative_offset: 0x6e,
|
||||
policy_tag_relative_offset: 0x7e,
|
||||
profile_tag_relative_offset: 0x9c,
|
||||
policy_chunk_len: 0x1a,
|
||||
profile_chunk_len: 0x20,
|
||||
},
|
||||
],
|
||||
evidence: vec![],
|
||||
});
|
||||
report.save_placed_structure_collection_header_probe =
|
||||
Some(SmpSaveTaggedCollectionHeaderProbe {
|
||||
profile_family: "rt3-105-save-container-v1".to_string(),
|
||||
|
|
@ -16916,6 +17071,11 @@ mod tests {
|
|||
note.contains("tagged region header reports live_record_count=145")
|
||||
})
|
||||
);
|
||||
assert!(slice.notes.iter().any(|note| {
|
||||
note.contains(
|
||||
"tagged region records also expose 2 repeated 0x55f1/0x55f2/0x55f3 triplets",
|
||||
)
|
||||
}));
|
||||
assert!(slice.notes.iter().any(|note| {
|
||||
note.contains("tagged placed-structure header reports live_record_count=2026")
|
||||
}));
|
||||
|
|
@ -17133,6 +17293,59 @@ mod tests {
|
|||
assert_eq!(directory_probe.entries[2].next_live_entry_id, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_region_record_triplet_probe_from_marker09_records() {
|
||||
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(&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 mut cursor = records_tag_offset + 4;
|
||||
for name in ["Marker09", "Marker10"] {
|
||||
bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes());
|
||||
bytes[cursor + 4] = name.len() as u8;
|
||||
bytes[cursor + 5..cursor + 5 + name.len()].copy_from_slice(name.as_bytes());
|
||||
cursor += 0x10;
|
||||
bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes());
|
||||
cursor += 0x1e;
|
||||
bytes[cursor..cursor + 2]
|
||||
.copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes());
|
||||
cursor += 0x40;
|
||||
}
|
||||
|
||||
let header_probe = 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,
|
||||
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: 0x96,
|
||||
live_id_bound_hex: "0x00000096".to_string(),
|
||||
live_record_count: 2,
|
||||
live_record_count_hex: "0x00000002".to_string(),
|
||||
header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x02],
|
||||
header_hex_words: vec![],
|
||||
evidence: vec![],
|
||||
};
|
||||
let triplet_probe = parse_save_region_record_triplet_probe(&bytes, Some(&header_probe))
|
||||
.expect("region triplet probe should parse");
|
||||
|
||||
assert_eq!(triplet_probe.record_count, 2);
|
||||
assert_eq!(triplet_probe.entries[0].name, "Marker09");
|
||||
assert_eq!(triplet_probe.entries[0].policy_tag_relative_offset, 0x10);
|
||||
assert_eq!(triplet_probe.entries[0].profile_tag_relative_offset, 0x2e);
|
||||
assert_eq!(triplet_probe.entries[1].name, "Marker10");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() {
|
||||
let mut bytes = vec![0u8; 0x400];
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ Working rule:
|
|||
## Next
|
||||
|
||||
- Reconstruct the save-side region record body on top of the newly corrected non-direct tagged
|
||||
region seam (`0x5209/0x520a/0x520b`, stride hint `0x06`, `Marker09` record stems), especially
|
||||
the pending bonus lane `[region+0x276]`, completion latch `[region+0x302]`, one-shot notice
|
||||
latch `[region+0x316]`, severity/source lane `[region+0x25e]`, and any stable region-id or
|
||||
class discriminator that can drive shellless city-connection service.
|
||||
region seam (`0x5209/0x520a/0x520b`, stride hint `0x06`, `Marker09` record stems) and its now
|
||||
grounded repeated `0x55f1/0x55f2/0x55f3` record-triplet envelope, especially the fixed `0x55f2`
|
||||
policy row behind `[region+0x272/+0x25a/+0x25e]`, the pending bonus lane `[region+0x276]`,
|
||||
completion latch `[region+0x302]`, one-shot notice latch `[region+0x316]`, severity/source lane
|
||||
`[region+0x25e]`, and any stable region-id or class discriminator that can drive shellless
|
||||
city-connection service.
|
||||
- Reconstruct the save-side placed-structure collection body on top of the newly grounded
|
||||
`0x36b1/0x36b2/0x36b3` header seam so the blocked city-connection / linked-transit branch can
|
||||
stop depending on atlas-only placed-structure and local-runtime refresh notes.
|
||||
|
|
@ -56,6 +58,9 @@ Working rule:
|
|||
at metadata dword `16`, while the actual region family is the larger non-direct `Marker09`
|
||||
collection with live_id/count `0x96/0x91`; the tagged placed-structure header
|
||||
(`0x36b1/0x36b2/0x36b3`) remains grounded alongside them.
|
||||
- That same corrected region seam now also exposes repeated `0x55f1/0x55f2/0x55f3` serialized
|
||||
record triplets with len-prefixed names plus fixed policy/profile chunk lengths, so the next
|
||||
city-connection pass can target the real record envelope instead of another blind scan.
|
||||
- Stepped calendar progression now also refreshes save-world owner time fields, including packed
|
||||
year, packed tuple words, absolute counter, and the derived selected-year gap scalar.
|
||||
- Automatic year-rollover calendar stepping now invokes periodic-boundary service.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue