From 934b0049eb93ea6f1eb77fe6faa16608a141b05d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 17:10:43 -0700 Subject: [PATCH] Probe raw save investor-confidence issue state --- README.md | 15 ++-- crates/rrt-runtime/src/lib.rs | 2 +- crates/rrt-runtime/src/smp.rs | 148 ++++++++++++++++++++++++++++++++++ docs/README.md | 11 ++- docs/runtime-rehost-plan.md | 13 +-- 5 files changed, 174 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 988dfcd..dad9193 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ documents can now also carry explicit company rosters and chairman-profile table company-targeted and chairman-targeted descriptor and condition batches can execute from standalone save-slice fixtures without overlay snapshots when that context is present; raw `.gms` inspection now reconstructs both collections automatically: the fixed save-side `0x32c8` world block still -supplies selected company/chairman ids plus the campaign override byte and chairman slot/role-gate -analysis bytes, and the tagged company / chairman-profile direct-record families now populate +supplies selected company/chairman ids plus the campaign override byte, the grounded issue-`0x37` +value/multiplier pair, and chairman slot/role-gate analysis bytes, and the tagged company / +chairman-profile direct-record families now populate save-native roster entries for real `.gms` imports and exports. The current raw-save boundary is narrower now: company/chairman identity, active flags, links, chairman cash, chairman holdings, chairman purchasing power, company debt, and company track-laying capacity are grounded directly @@ -42,9 +43,13 @@ surface also now exposes `runtime inspect-save-company-chairman ` for company/chairman scalar candidates, including fixed-world chairman slot / role-gate context, explicit company dword candidate windows, richer chairman qword cache views, and derived holdings-at-share-price / cached purchasing-power comparisons. The same fixed `0x32c8` world -block is now probed for its six-float economic tuning band too, but current atlas evidence still -keeps that editor-facing tuning family separate from the governance issue lanes behind investor -confidence and prime-rate math. A checked-in +block is now probed for both the grounded issue-`0x37` pair at `[world+0x29/+0x2d]` and the +separate six-float economic tuning band, but current atlas evidence still keeps that editor-facing +tuning family distinct from the governance issue lanes behind investor confidence and prime-rate +math. A checked-in +The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we +should prefer rehosting the owning source state or the real reader/setter family rather than +guessing one more derived leaf field from nearby offsets. A checked-in `EventEffects` export now exists too in `artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now exists beside it in `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`. Recovered diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 36245d7..babe773 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -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, diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index a64e34d..55c9905 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -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, } +#[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, +} + #[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, #[serde(default)] + pub world_issue_37: Option, + #[serde(default)] pub world_economic_tuning: Option, #[serde(default)] pub company_entries: Vec, @@ -2543,6 +2561,7 @@ pub struct SmpInspectionReport { pub rt3_105_post_span_bridge_probe: Option, pub rt3_105_save_bridge_payload_probe: Option, pub save_world_selection_context_probe: Option, + pub save_world_issue_37_probe: Option, pub save_world_economic_tuning_probe: Option, pub save_company_collection_header_probe: Option, pub save_chairman_profile_collection_header_probe: Option, @@ -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 { 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) -> 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) -> 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 { + 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(&[]); diff --git a/docs/README.md b/docs/README.md index fe7b291..de61aad 100644 --- a/docs/README.md +++ b/docs/README.md @@ -98,8 +98,8 @@ The highest-value next passes are now: can execute from standalone save-slice fixtures without overlay snapshots when that context is present; raw `.gms` inspection/export now reconstructs those company/chairman collections directly from save-side tagged record families, while the fixed save-side `0x32c8` world block - still provides selected company/chairman ids plus the grounded campaign-override and - chairman-slot / role-gate analysis bytes; the current raw-save frontier is narrower now: + still provides selected company/chairman ids plus the grounded campaign-override, issue-`0x37` + value/multiplier, and chairman-slot / role-gate analysis bytes; the current raw-save frontier is narrower now: company/chairman identity, active flags, links, chairman cash, chairman holdings, chairman purchasing power, company debt, company track-laying capacity, and collection counts are grounded, while broader company @@ -107,9 +107,12 @@ The highest-value next passes are now: defaults until their raw offsets are pinned more strongly; the offline analysis command `runtime inspect-save-company-chairman ` now dumps those remaining raw record candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate - context, the fixed-world six-float economic tuning band, derived holdings-at-share-price and - cached purchasing-power totals, + context, the grounded fixed-world issue-`0x37` pair, the separate six-float economic tuning + band, derived holdings-at-share-price and cached purchasing-power totals, context, company dword candidate windows, and richer chairman qword cache views +- the project rule on the remaining closure work is now explicit too: when one runtime-facing field + is still ambiguous, prefer rehosting the owning source state or real reader/setter family first + instead of guessing another derived leaf field from neighboring raw offsets - a checked-in `EventEffects` export now exists at `artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json` diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 09bb3b8..691b169 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -60,15 +60,16 @@ Implemented today: chairman-targeted descriptor/condition batches execute from standalone save-slice fixtures without overlay snapshots when the checked-in documents include that context, and raw `.gms` inspection/export now reconstructs company/chairman direct-record entries too; the fixed - `0x32c8` world block still contributes selected ids plus the grounded campaign-override and - chairman slot / role-gate analysis bytes, while the tagged company and chairman/profile + `0x32c8` world block still contributes selected ids plus the grounded campaign-override, + issue-`0x37` value/multiplier, and chairman slot / role-gate analysis bytes, while the tagged company and chairman/profile collections now provide save-native roster entries and `observed_entry_count`; raw company debt from the bond table and raw company track-laying capacity from the record tail are grounded too, and chairman purchasing power now reuses the strongest nonnegative cached qword total from the `[profile+0x1e9..]` band plus current cash instead of collapsing to plain net worth; - the same fixed world payload now exposes the six-float economic tuning band - `[world+0x0be2..+0x0bf6]` through save inspection too, but current atlas evidence still keeps - that editor-tuning family separate from the company-governance issue lanes; + the same fixed world payload now exposes the grounded issue-`0x37` pair at `[world+0x29/+0x2d]` + and the separate six-float economic tuning band `[world+0x0be2..+0x0bf6]` through save + inspection too, but current atlas evidence still keeps that editor-tuning family separate from + the company-governance issue lanes; and `runtime inspect-save-company-chairman ` now exposes the remaining raw company/chairman scalar candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate context, company dword candidate windows, richer chairman qword @@ -521,6 +522,8 @@ The currently implemented normalized runtime surface is: normalized runtime-effect vocabulary with staged event-record mutation - save-side inspection and partial state projection for `.smp` inputs, including per-record packed event summaries and selective executable import +- closure policy on the remaining frontier: prefer rehosting the owning source state and the real + reader/setter family over guessing one more derived leaf metric from nearby save offsets - tracked save-slice documents plus save-slice-backed fixture loading for captured-runtime coverage Checked-in fixture families already include: