Probe raw save investor-confidence issue state

This commit is contained in:
Jan Petykiewicz 2026-04-17 17:10:43 -07:00
commit 934b0049eb
5 changed files with 174 additions and 15 deletions

View file

@ -85,7 +85,7 @@ pub use smp::{
SmpSaveChairmanRecordAnalysisEntry, SmpSaveCompanyChairmanAnalysisReport,
SmpSaveCompanyRecordAnalysisEntry, SmpSaveDwordCandidate, SmpSaveLoadCandidateTableSummary,
SmpSaveLoadSummary, SmpSaveScalarCandidate, SmpSaveTaggedCollectionHeaderProbe,
SmpSaveWorldEconomicTuningProbe, SmpSaveWorldSelectionRoleAnalysis,
SmpSaveWorldEconomicTuningProbe, SmpSaveWorldIssue37Probe, SmpSaveWorldSelectionRoleAnalysis,
SmpSaveWorldSelectionRoleAnalysisEntry, SmpSecondaryVariantProbe, SmpSharedHeader,
SmpSpecialConditionEntry, SmpSpecialConditionsProbe,
inspect_save_company_and_chairman_analysis_bytes,

View file

@ -95,6 +95,8 @@ 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 RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET: usize = 0x25;
const RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET: usize = 0x29;
const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET: usize = 0x83;
const RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET: usize = 0xc1;
const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET: usize = 0x0bbf;
@ -1496,6 +1498,20 @@ pub struct SmpSaveWorldEconomicTuningProbe {
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpSaveWorldIssue37Probe {
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 issue_value_lane: SmpSaveDwordCandidate,
pub multiplier_lane: SmpSaveDwordCandidate,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveTaggedCollectionHeaderProbe {
pub profile_family: String,
@ -2296,6 +2312,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
#[serde(default)]
pub world_selection_context: Option<SmpSaveWorldSelectionRoleAnalysis>,
#[serde(default)]
pub world_issue_37: Option<SmpSaveWorldIssue37Probe>,
#[serde(default)]
pub world_economic_tuning: Option<SmpSaveWorldEconomicTuningProbe>,
#[serde(default)]
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
@ -2543,6 +2561,7 @@ pub struct SmpInspectionReport {
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 save_world_issue_37_probe: Option<SmpSaveWorldIssue37Probe>,
pub save_world_economic_tuning_probe: Option<SmpSaveWorldEconomicTuningProbe>,
pub save_company_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_chairman_profile_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
@ -2750,6 +2769,15 @@ pub fn load_save_slice_from_report(
);
}
}
if let Some(probe) = &report.save_world_issue_37_probe {
notes.push(format!(
"Raw save fixed world block also exposes the grounded issue-0x37 pair: value={} at file offset 0x{:x} and companion multiplier {:.6} at file offset 0x{:x}.",
probe.issue_value_lane.value_i32,
probe.payload_offset + probe.issue_value_lane.relative_offset,
probe.multiplier_lane.value_f32,
probe.payload_offset + probe.multiplier_lane.relative_offset
));
}
if let Some(probe) = &report.save_world_economic_tuning_probe {
notes.push(format!(
"Raw save fixed world block also exposes the six-lane economic tuning float band at file offset 0x{:x} (mirror lane at 0x{:x}).",
@ -2837,6 +2865,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
) -> Option<SmpSaveCompanyChairmanAnalysisReport> {
let selection_probe = report.save_world_selection_context_probe.as_ref();
let world_selection_context = selection_probe.map(build_save_world_selection_role_analysis);
let world_issue_37 = report.save_world_issue_37_probe.clone();
let world_economic_tuning = report.save_world_economic_tuning_probe.clone();
let company_header_probe = report.save_company_collection_header_probe.as_ref();
let chairman_header_probe = report
@ -3048,6 +3077,12 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
.to_string(),
);
}
if world_issue_37.is_some() {
notes.push(
"World analysis now also exports the grounded issue-0x37 pair from the same 0x32c8 world payload: the clamped small issue value at [world+0x2d] and its companion multiplier lane at [world+0x29]."
.to_string(),
);
}
if world_economic_tuning.is_some() {
notes.push(
"World analysis now also exports the fixed six-lane economic tuning float block from the same 0x32c8 world payload; current atlas evidence still treats that band as distinct from the issue-0x37 investor-confidence family."
@ -3089,6 +3124,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
selected_chairman_profile_id: selection_probe
.map(|probe| probe.selected_chairman_profile_id),
world_selection_context,
world_issue_37,
world_economic_tuning,
company_entries,
chairman_entries,
@ -6454,6 +6490,11 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_world_issue_37_probe = parse_save_world_issue_37_probe(
bytes,
file_extension_hint.as_deref(),
container_profile.as_ref(),
);
let save_world_economic_tuning_probe = parse_save_world_economic_tuning_probe(
bytes,
file_extension_hint.as_deref(),
@ -6628,6 +6669,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
rt3_105_post_span_bridge_probe,
rt3_105_save_bridge_payload_probe,
save_world_selection_context_probe,
save_world_issue_37_probe,
save_world_economic_tuning_probe,
save_company_collection_header_probe,
save_chairman_profile_collection_header_probe,
@ -8160,6 +8202,76 @@ fn parse_save_world_selection_context_probe(
None
}
fn parse_save_world_issue_37_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
container_profile: Option<&SmpContainerProfile>,
) -> Option<SmpSaveWorldIssue37Probe> {
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 issue_value_lane = build_save_dword_candidate(
bytes,
payload_offset,
"issue_0x37_value",
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET,
)?;
let multiplier_lane = build_save_dword_candidate(
bytes,
payload_offset,
"issue_0x37_multiplier",
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET,
)?;
return Some(SmpSaveWorldIssue37Probe {
profile_family: profile.profile_family.clone(),
source_kind: "save-direct-world-block".to_string(),
semantic_family: "scenario-save-world-issue-0x37".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),
issue_value_lane,
multiplier_lane,
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!(
"issue value lane uses payload +0x{:x} ([world+0x2d]); atlas notes tie 0x004339b0 to the clamped 0..4 issue-0x37 setter there",
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET
),
format!(
"multiplier lane uses payload +0x{:x} ([world+0x29]); atlas notes tie 0x004339b0 to one companion scalar at that lane before company share-price refresh",
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET
),
],
});
}
None
}
fn parse_save_world_economic_tuning_probe(
bytes: &[u8],
file_extension_hint: Option<&str>,
@ -14812,6 +14924,42 @@ mod tests {
assert_eq!(probe.tuning_lanes[5].value_f32, 15.25);
}
#[test]
fn parses_save_world_issue_37_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_ISSUE_0X37_VALUE_RELATIVE_OFFSET
..payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 4]
.copy_from_slice(&3u32.to_le_bytes());
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET
..payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET + 4]
.copy_from_slice(&0.06f32.to_bits().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_issue_37_probe(
&bytes,
Some("gms"),
Some(&SmpContainerProfile {
profile_family: "rt3-105-save-container-v1".to_string(),
profile_evidence: vec![],
is_known_profile: true,
}),
)
.expect("world issue-0x37 probe should parse");
assert_eq!(probe.chunk_tag_offset, chunk_tag_offset);
assert_eq!(probe.payload_offset, payload_offset);
assert_eq!(probe.issue_value_lane.relative_offset_hex, "0x29");
assert_eq!(probe.issue_value_lane.value_i32, 3);
assert_eq!(probe.multiplier_lane.relative_offset_hex, "0x25");
assert!((probe.multiplier_lane.value_f32 - 0.06).abs() < f32::EPSILON);
}
#[test]
fn loads_selection_only_company_and_chairman_context_from_save_world_probe() {
let mut report = inspect_smp_bytes(&[]);