Probe raw save selection context

This commit is contained in:
Jan Petykiewicz 2026-04-17 09:41:16 -07:00
commit 40c0e94ad5
9 changed files with 581 additions and 137 deletions

View file

@ -97,9 +97,11 @@ struct SaveSliceProjection {
event_runtime_records: Vec<RuntimeEventRecord>,
companies: Vec<RuntimeCompany>,
has_company_projection: bool,
has_company_selection_override: bool,
selected_company_id: Option<u32>,
chairman_profiles: Vec<RuntimeChairmanProfile>,
has_chairman_projection: bool,
has_chairman_selection_override: bool,
selected_chairman_profile_id: Option<u32>,
candidate_availability: BTreeMap<String, u32>,
named_locomotive_availability: BTreeMap<String, u32>,
@ -269,11 +271,19 @@ pub fn project_save_slice_to_runtime_state_import(
world_restore: projection.world_restore,
metadata: projection.metadata,
companies: projection.companies,
selected_company_id: projection.selected_company_id,
selected_company_id: if projection.has_company_projection {
projection.selected_company_id
} else {
None
},
players: Vec::new(),
selected_player_id: None,
chairman_profiles: projection.chairman_profiles,
selected_chairman_profile_id: projection.selected_chairman_profile_id,
selected_chairman_profile_id: if projection.has_chairman_projection {
projection.selected_chairman_profile_id
} else {
None
},
trains: Vec::new(),
locomotive_catalog: projection.locomotive_catalog.unwrap_or_default(),
cargo_catalog: projection.cargo_catalog.unwrap_or_default(),
@ -349,7 +359,9 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
} else {
base_state.companies.clone()
},
selected_company_id: if projection.has_company_projection {
selected_company_id: if projection.has_company_projection
|| projection.has_company_selection_override
{
projection.selected_company_id
} else {
base_state.selected_company_id
@ -361,7 +373,9 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
} else {
base_state.chairman_profiles.clone()
},
selected_chairman_profile_id: if projection.has_chairman_projection {
selected_chairman_profile_id: if projection.has_chairman_projection
|| projection.has_chairman_selection_override
{
projection.selected_chairman_profile_id
} else {
base_state.selected_chairman_profile_id
@ -809,7 +823,7 @@ fn project_save_slice_components(
None
};
let (companies, has_company_projection, selected_company_id) =
let (companies, has_company_projection, has_company_selection_override, selected_company_id) =
if let Some(roster) = &save_slice.company_roster {
metadata.insert(
"save_slice.company_roster_source_kind".to_string(),
@ -829,55 +843,77 @@ fn project_save_slice_components(
selected_company_id.to_string(),
);
}
(
roster
.entries
.iter()
.map(|entry| RuntimeCompany {
company_id: entry.company_id,
current_cash: entry.current_cash,
debt: entry.debt,
credit_rating_score: entry.credit_rating_score,
prime_rate: entry.prime_rate,
active: entry.active,
available_track_laying_capacity: entry.available_track_laying_capacity,
controller_kind: entry.controller_kind,
linked_chairman_profile_id: entry.linked_chairman_profile_id,
book_value_per_share: entry.book_value_per_share,
investor_confidence: entry.investor_confidence,
management_attitude: entry.management_attitude,
takeover_cooldown_year: entry.takeover_cooldown_year,
merger_cooldown_year: entry.merger_cooldown_year,
track_piece_counts: entry.track_piece_counts,
})
.collect::<Vec<_>>(),
true,
roster.selected_company_id,
)
if roster.entries.is_empty() {
(
Vec::new(),
false,
roster.selected_company_id.is_some(),
roster.selected_company_id,
)
} else {
(
roster
.entries
.iter()
.map(|entry| RuntimeCompany {
company_id: entry.company_id,
current_cash: entry.current_cash,
debt: entry.debt,
credit_rating_score: entry.credit_rating_score,
prime_rate: entry.prime_rate,
active: entry.active,
available_track_laying_capacity: entry.available_track_laying_capacity,
controller_kind: entry.controller_kind,
linked_chairman_profile_id: entry.linked_chairman_profile_id,
book_value_per_share: entry.book_value_per_share,
investor_confidence: entry.investor_confidence,
management_attitude: entry.management_attitude,
takeover_cooldown_year: entry.takeover_cooldown_year,
merger_cooldown_year: entry.merger_cooldown_year,
track_piece_counts: entry.track_piece_counts,
})
.collect::<Vec<_>>(),
true,
roster.selected_company_id.is_some(),
roster.selected_company_id,
)
}
} else {
(Vec::new(), false, None)
(Vec::new(), false, false, None)
};
let (chairman_profiles, has_chairman_projection, selected_chairman_profile_id) =
if let Some(table) = &save_slice.chairman_profile_table {
let (
chairman_profiles,
has_chairman_projection,
has_chairman_selection_override,
selected_chairman_profile_id,
) = if let Some(table) = &save_slice.chairman_profile_table {
metadata.insert(
"save_slice.chairman_profile_table_source_kind".to_string(),
table.source_kind.clone(),
);
metadata.insert(
"save_slice.chairman_profile_table_semantic_family".to_string(),
table.semantic_family.clone(),
);
metadata.insert(
"save_slice.chairman_profile_table_entry_count".to_string(),
table.observed_entry_count.to_string(),
);
if let Some(selected_chairman_profile_id) = table.selected_chairman_profile_id {
metadata.insert(
"save_slice.chairman_profile_table_source_kind".to_string(),
table.source_kind.clone(),
"save_slice.selected_chairman_profile_id".to_string(),
selected_chairman_profile_id.to_string(),
);
metadata.insert(
"save_slice.chairman_profile_table_semantic_family".to_string(),
table.semantic_family.clone(),
);
metadata.insert(
"save_slice.chairman_profile_table_entry_count".to_string(),
table.observed_entry_count.to_string(),
);
if let Some(selected_chairman_profile_id) = table.selected_chairman_profile_id {
metadata.insert(
"save_slice.selected_chairman_profile_id".to_string(),
selected_chairman_profile_id.to_string(),
);
}
}
if table.entries.is_empty() {
(
Vec::new(),
false,
table.selected_chairman_profile_id.is_some(),
table.selected_chairman_profile_id,
)
} else {
(
table
.entries
@ -895,11 +931,13 @@ fn project_save_slice_components(
})
.collect::<Vec<_>>(),
true,
table.selected_chairman_profile_id.is_some(),
table.selected_chairman_profile_id,
)
} else {
(Vec::new(), false, None)
};
}
} else {
(Vec::new(), false, false, None)
};
let named_locomotive_cost = BTreeMap::new();
let all_cargo_price_override = None;
@ -920,6 +958,8 @@ fn project_save_slice_components(
&& companies
.iter()
.all(|company| company.controller_kind != RuntimeCompanyControllerKind::Unknown);
} else if has_company_selection_override {
packed_event_context.selected_company_id = selected_company_id;
}
if has_chairman_projection {
packed_event_context.known_chairman_profile_ids = chairman_profiles
@ -927,6 +967,8 @@ fn project_save_slice_components(
.map(|profile| profile.profile_id)
.collect();
packed_event_context.selected_chairman_profile_id = selected_chairman_profile_id;
} else if has_chairman_selection_override {
packed_event_context.selected_chairman_profile_id = selected_chairman_profile_id;
}
if let Some(catalog) = &locomotive_catalog {
packed_event_context.locomotive_catalog_names_by_id = catalog
@ -976,9 +1018,11 @@ fn project_save_slice_components(
event_runtime_records,
companies,
has_company_projection,
has_company_selection_override,
selected_company_id,
chairman_profiles,
has_chairman_projection,
has_chairman_selection_override,
selected_chairman_profile_id,
candidate_availability,
named_locomotive_availability,
@ -4918,11 +4962,12 @@ mod tests {
descriptor_id: u32,
value: i32,
) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
let slot = descriptor_id.saturating_sub(105);
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
row_index: 0,
descriptor_id,
descriptor_label: Some("Unknown Cargo Price".to_string()),
descriptor_label: Some(format!("Named Cargo Price Slot {slot}")),
target_mask_bits: Some(0x08),
parameter_family: Some("cargo_price_scalar".to_string()),
grouped_target_subject: None,
@ -4937,7 +4982,7 @@ mod tests {
value_word_0x16: 0,
row_shape: "scalar_assignment".to_string(),
semantic_family: Some("scalar_assignment".to_string()),
semantic_preview: Some(format!("Set Unknown Cargo Price to {value}")),
semantic_preview: Some(format!("Set Named Cargo Price Slot {slot} to {value}")),
recovered_cargo_slot: None,
recovered_cargo_class: None,
recovered_cargo_label: None,
@ -5874,6 +5919,118 @@ mod tests {
assert_eq!(import.state.territories, base_state.territories);
}
#[test]
fn overlay_applies_selection_only_company_and_chairman_context_from_save_slice() {
let base_state = RuntimeState {
companies: vec![
crate::RuntimeCompany {
company_id: 1,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
linked_chairman_profile_id: Some(1),
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
},
crate::RuntimeCompany {
company_id: 42,
current_cash: 200,
debt: 0,
credit_rating_score: None,
prime_rate: None,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
linked_chairman_profile_id: Some(9),
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
},
],
selected_company_id: Some(42),
chairman_profiles: vec![
crate::RuntimeChairmanProfile {
profile_id: 1,
name: "Selected".to_string(),
active: true,
current_cash: 0,
linked_company_id: Some(1),
company_holdings: BTreeMap::new(),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
},
crate::RuntimeChairmanProfile {
profile_id: 9,
name: "Base".to_string(),
active: true,
current_cash: 0,
linked_company_id: Some(42),
company_holdings: BTreeMap::new(),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
},
],
selected_chairman_profile_id: Some(9),
..state()
};
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-105-save-container-v1".to_string()),
mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(),
mechanism_confidence: "mixed".to_string(),
trailer_family: Some("rt3-105-save-trailer-v1".to_string()),
bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()),
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
company_roster: Some(crate::SmpLoadedCompanyRoster {
source_kind: "save-direct-world-block-company-selection-only".to_string(),
semantic_family: "scenario-selected-company-context".to_string(),
observed_entry_count: 0,
selected_company_id: Some(1),
entries: Vec::new(),
}),
chairman_profile_table: Some(crate::SmpLoadedChairmanProfileTable {
source_kind: "save-direct-world-block-chairman-selection-only".to_string(),
semantic_family: "scenario-selected-chairman-context".to_string(),
observed_entry_count: 0,
selected_chairman_profile_id: Some(1),
entries: Vec::new(),
}),
special_conditions_table: None,
event_runtime_collection: None,
notes: vec![],
};
let import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"overlay-save-selection-only-context",
None,
)
.expect("overlay import should project");
assert_eq!(import.state.companies, base_state.companies);
assert_eq!(import.state.selected_company_id, Some(1));
assert_eq!(import.state.chairman_profiles, base_state.chairman_profiles);
assert_eq!(import.state.selected_chairman_profile_id, Some(1));
}
#[test]
fn projects_executable_packed_records_into_runtime_and_services_follow_on() {
let save_slice = SmpLoadedSaveSlice {

View file

@ -90,6 +90,11 @@ 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 RT3_SAVE_WORLD_BLOCK_CHUNK_TAG: u32 = 0x000032c8;
const RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG: u32 = 0x000032c9;
const RT3_SAVE_WORLD_BLOCK_LEN: usize = 0x4f2c;
const RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET: usize = 0x1d;
const RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET: usize = 0x21;
const EVENT_RUNTIME_COLLECTION_METADATA_TAG: u16 = 0x4e99;
const EVENT_RUNTIME_COLLECTION_RECORDS_TAG: u16 = 0x4e9a;
const EVENT_RUNTIME_COLLECTION_CLOSE_TAG: u16 = 0x4e9b;
@ -1444,6 +1449,24 @@ pub struct SmpRt3105SaveBridgePayloadProbe {
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveWorldSelectionContextProbe {
pub profile_family: String,
pub source_kind: String,
pub semantic_family: String,
pub chunk_tag_offset: usize,
pub payload_offset: usize,
pub payload_len: usize,
pub payload_len_hex: String,
pub selected_company_id_offset: usize,
pub selected_company_id: u32,
pub selected_company_id_hex: String,
pub selected_chairman_profile_id_offset: usize,
pub selected_chairman_profile_id: u32,
pub selected_chairman_profile_id_hex: String,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpRt3105SaveNameTableProbe {
pub profile_family: String,
@ -2362,6 +2385,7 @@ pub struct SmpInspectionReport {
pub runtime_post_span_probe: Option<SmpRuntimePostSpanProbe>,
pub rt3_105_post_span_bridge_probe: Option<SmpRt3105PostSpanBridgeProbe>,
pub rt3_105_save_bridge_payload_probe: Option<SmpRt3105SaveBridgePayloadProbe>,
pub save_world_selection_context_probe: Option<SmpSaveWorldSelectionContextProbe>,
pub rt3_105_save_name_table_probe: Option<SmpRt3105SaveNameTableProbe>,
pub rt3_105_save_named_locomotive_availability_probe:
Option<SmpRt3105SaveNamedLocomotiveAvailabilityProbe>,
@ -2497,6 +2521,14 @@ pub fn load_save_slice_from_report(
.recipe_book_summary_probe
.as_ref()
.and_then(derive_cargo_catalog_from_recipe_book_probe);
let company_roster = report
.save_world_selection_context_probe
.as_ref()
.and_then(derive_selection_only_company_roster_from_save_world_probe);
let chairman_profile_table = report
.save_world_selection_context_probe
.as_ref()
.and_then(derive_selection_only_chairman_profile_table_from_save_world_probe);
let special_conditions_table =
report
.special_conditions_probe
@ -2509,6 +2541,21 @@ pub fn load_save_slice_from_report(
enabled_visible_labels: probe.enabled_visible_labels.clone(),
entries: probe.entries.clone(),
});
let mut notes = summary.notes.clone();
if let Some(probe) = &report.save_world_selection_context_probe {
notes.push(format!(
"Raw save fixed world block exposes selected_company_id={} at file offset 0x{:x}.",
probe.selected_company_id, probe.selected_company_id_offset
));
notes.push(format!(
"Raw save fixed world block exposes selected_chairman_profile_id={} at file offset 0x{:x}.",
probe.selected_chairman_profile_id, probe.selected_chairman_profile_id_offset
));
notes.push(
"Raw save inspection still does not reconstruct full company_roster or chairman_profile_table payloads; the grounded package-save path only proves selection ids and header-level collection state for those families."
.to_string(),
);
}
Ok(SmpLoadedSaveSlice {
file_extension_hint: summary.file_extension_hint.clone(),
@ -2522,11 +2569,11 @@ pub fn load_save_slice_from_report(
named_locomotive_availability_table,
locomotive_catalog,
cargo_catalog,
company_roster: None,
chairman_profile_table: None,
company_roster,
chairman_profile_table,
special_conditions_table,
event_runtime_collection: report.event_runtime_collection_summary.clone(),
notes: summary.notes.clone(),
notes,
})
}
@ -2609,6 +2656,30 @@ fn derive_cargo_catalog_from_recipe_book_probe(
})
}
fn derive_selection_only_company_roster_from_save_world_probe(
probe: &SmpSaveWorldSelectionContextProbe,
) -> Option<SmpLoadedCompanyRoster> {
Some(SmpLoadedCompanyRoster {
source_kind: format!("{}-company-selection-only", probe.source_kind),
semantic_family: "scenario-selected-company-context".to_string(),
observed_entry_count: 0,
selected_company_id: Some(probe.selected_company_id),
entries: Vec::new(),
})
}
fn derive_selection_only_chairman_profile_table_from_save_world_probe(
probe: &SmpSaveWorldSelectionContextProbe,
) -> Option<SmpLoadedChairmanProfileTable> {
Some(SmpLoadedChairmanProfileTable {
source_kind: format!("{}-chairman-selection-only", probe.source_kind),
semantic_family: "scenario-selected-chairman-context".to_string(),
observed_entry_count: 0,
selected_chairman_profile_id: Some(probe.selected_chairman_profile_id),
entries: Vec::new(),
})
}
fn known_cargo_slot_definition(slot_id: u32) -> Option<KnownCargoSlotDefinition> {
KNOWN_CARGO_SLOT_DEFINITIONS
.iter()
@ -5264,6 +5335,11 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
);
let rt3_105_save_bridge_payload_probe =
parse_rt3_105_save_bridge_payload_probe(bytes, rt3_105_post_span_bridge_probe.as_ref());
let save_world_selection_context_probe = parse_save_world_selection_context_probe(
bytes,
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let rt3_105_save_name_table_probe = parse_rt3_105_save_name_table_probe(
bytes,
file_extension_hint.as_deref(),
@ -5410,6 +5486,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
runtime_post_span_probe,
rt3_105_post_span_bridge_probe,
rt3_105_save_bridge_payload_probe,
save_world_selection_context_probe,
rt3_105_save_name_table_probe,
rt3_105_save_named_locomotive_availability_probe,
special_conditions_probe,
@ -6826,6 +6903,74 @@ fn parse_rt3_105_save_bridge_payload_probe(
})
}
fn parse_save_world_selection_context_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
container_profile: Option<&SmpContainerProfile>,
) -> Option<SmpSaveWorldSelectionContextProbe> {
if file_extension_hint != Some("gms") {
return None;
}
let profile = container_profile?;
let supported = matches!(
profile.profile_family.as_str(),
"rt3-classic-save-container-v1"
| "rt3-105-save-container-v1"
| "rt3-105-scenario-save-container-v1"
| "rt3-105-alt-save-container-v1"
);
if !supported {
return None;
}
for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) {
let payload_offset = chunk_tag_offset + 4;
let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?;
if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) {
continue;
}
let selected_company_id_offset =
payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET;
let selected_chairman_profile_id_offset =
payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET;
let selected_company_id = read_u32_at(bytes, selected_company_id_offset)?;
let selected_chairman_profile_id = read_u32_at(bytes, selected_chairman_profile_id_offset)?;
return Some(SmpSaveWorldSelectionContextProbe {
profile_family: profile.profile_family.clone(),
source_kind: "save-direct-world-block".to_string(),
semantic_family: "scenario-selected-company-and-chairman-context".to_string(),
chunk_tag_offset,
payload_offset,
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
selected_company_id_offset,
selected_company_id,
selected_company_id_hex: format!("0x{selected_company_id:08x}"),
selected_chairman_profile_id_offset,
selected_chairman_profile_id,
selected_chairman_profile_id_hex: format!("0x{selected_chairman_profile_id:08x}"),
evidence: vec![
format!(
"chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block"
),
format!(
"next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span"
),
format!(
"selected company id comes from payload +0x{:x} ([world+0x21])",
RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET
),
format!(
"selected chairman profile id comes from payload +0x{:x} ([world+0x25])",
RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET
),
],
});
}
None
}
fn parse_rt3_105_save_name_table_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
@ -8619,6 +8764,15 @@ fn find_u16_le_offsets(bytes: &[u8], needle: u16) -> Vec<usize> {
.collect()
}
fn find_u32_le_offsets(bytes: &[u8], needle: u32) -> Vec<usize> {
let pattern = needle.to_le_bytes();
bytes
.windows(pattern.len())
.enumerate()
.filter_map(|(offset, window)| (window == pattern).then_some(offset))
.collect()
}
fn find_next_nonzero_offset(bytes: &[u8], start: usize) -> Option<usize> {
bytes
.iter()
@ -13025,6 +13179,114 @@ mod tests {
);
}
#[test]
fn parses_save_world_selection_context_probe_from_fixed_world_block() {
let mut bytes = vec![0u8; 0x8000];
let chunk_tag_offset = 0x3ceusize;
let payload_offset = chunk_tag_offset + 4;
bytes[chunk_tag_offset..chunk_tag_offset + 4]
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes());
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET
..payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET + 4]
.copy_from_slice(&7u32.to_le_bytes());
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET
..payload_offset
+ RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET
+ 4]
.copy_from_slice(&9u32.to_le_bytes());
let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN;
bytes[next_chunk_offset..next_chunk_offset + 4]
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes());
let probe = parse_save_world_selection_context_probe(
&bytes,
Some("gms"),
Some(&SmpContainerProfile {
profile_family: "rt3-105-save-container-v1".to_string(),
profile_evidence: vec![],
is_known_profile: true,
}),
)
.expect("selection-context probe should parse");
assert_eq!(probe.chunk_tag_offset, chunk_tag_offset);
assert_eq!(probe.payload_offset, payload_offset);
assert_eq!(probe.selected_company_id, 7);
assert_eq!(probe.selected_chairman_profile_id, 9);
}
#[test]
fn loads_selection_only_company_and_chairman_context_from_save_world_probe() {
let mut report = inspect_smp_bytes(&[]);
report.save_load_summary = Some(SmpSaveLoadSummary {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-105-save-container-v1".to_string()),
mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(),
mechanism_confidence: "mixed".to_string(),
packed_profile_kind: None,
packed_profile_family: None,
packed_profile_offset: None,
packed_profile_len: None,
map_path: None,
display_name: None,
profile_byte_0x77: None,
profile_byte_0x77_hex: None,
profile_byte_0x82: None,
profile_byte_0x82_hex: None,
profile_byte_0x97: None,
profile_byte_0x97_hex: None,
profile_byte_0xc5: None,
profile_byte_0xc5_hex: None,
trailer_family: Some("rt3-105-save-trailer-v1".to_string()),
bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()),
candidate_table: None,
notes: vec![],
});
report.save_world_selection_context_probe = Some(SmpSaveWorldSelectionContextProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-direct-world-block".to_string(),
semantic_family: "scenario-selected-company-and-chairman-context".to_string(),
chunk_tag_offset: 0x3ce,
payload_offset: 0x3d2,
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
selected_company_id_offset: 0x3ef,
selected_company_id: 1,
selected_company_id_hex: "0x00000001".to_string(),
selected_chairman_profile_id_offset: 0x3f3,
selected_chairman_profile_id: 9,
selected_chairman_profile_id_hex: "0x00000009".to_string(),
evidence: vec![],
});
let slice = load_save_slice_from_report(&report).expect("save slice");
let company_roster = slice.company_roster.expect("selection-only company roster");
assert_eq!(company_roster.observed_entry_count, 0);
assert_eq!(company_roster.selected_company_id, Some(1));
assert!(company_roster.entries.is_empty());
let chairman_table = slice
.chairman_profile_table
.expect("selection-only chairman table");
assert_eq!(chairman_table.observed_entry_count, 0);
assert_eq!(chairman_table.selected_chairman_profile_id, Some(9));
assert!(chairman_table.entries.is_empty());
assert!(
slice
.notes
.iter()
.any(|note| note.contains("selected_company_id=1"))
);
assert!(
slice
.notes
.iter()
.any(|note| note.contains("selected_chairman_profile_id=9"))
);
}
#[test]
fn classifies_rt3_105_post_span_bridge_variants() {
let base_trailer = SmpRuntimeTrailerBlock {