Expand raw save company-chairman analysis surfaces

This commit is contained in:
Jan Petykiewicz 2026-04-17 15:18:13 -07:00
commit 1525703cd1
5 changed files with 219 additions and 14 deletions

View file

@ -38,7 +38,8 @@ company debt, and company track-laying capacity are grounded directly from save
broader company finance/governance scalars and controller-kind reconstruction still remain broader company finance/governance scalars and controller-kind reconstruction still remain
conservative defaults until their raw lanes are pinned more strongly. The offline runtime analysis conservative defaults until their raw lanes are pinned more strongly. The offline runtime analysis
surface also now exposes `runtime inspect-save-company-chairman <save.gms>` for those remaining raw surface also now exposes `runtime inspect-save-company-chairman <save.gms>` for those remaining raw
company/chairman scalar candidates. A checked-in company/chairman scalar candidates, including fixed-world chairman slot / role-gate context,
explicit company dword candidate windows, and richer chairman qword cache views. A checked-in
`EventEffects` export now exists too in `EventEffects` export now exists too in
`artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now `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 exists beside it in `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`. Recovered

View file

@ -83,9 +83,10 @@ pub use smp::{
SmpRuntimeAnchorCycleBlock, SmpRuntimePostSpanHeaderCandidate, SmpRuntimePostSpanProbe, SmpRuntimeAnchorCycleBlock, SmpRuntimePostSpanHeaderCandidate, SmpRuntimePostSpanProbe,
SmpRuntimeTrailerBlock, SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock, SmpRuntimeTrailerBlock, SmpSaveAnchorRunBlock, SmpSaveBootstrapBlock,
SmpSaveChairmanRecordAnalysisEntry, SmpSaveCompanyChairmanAnalysisReport, SmpSaveChairmanRecordAnalysisEntry, SmpSaveCompanyChairmanAnalysisReport,
SmpSaveCompanyRecordAnalysisEntry, SmpSaveLoadCandidateTableSummary, SmpSaveLoadSummary, SmpSaveCompanyRecordAnalysisEntry, SmpSaveDwordCandidate, SmpSaveLoadCandidateTableSummary,
SmpSaveScalarCandidate, SmpSaveTaggedCollectionHeaderProbe, SmpSecondaryVariantProbe, SmpSaveLoadSummary, SmpSaveScalarCandidate, SmpSaveTaggedCollectionHeaderProbe,
SmpSharedHeader, SmpSpecialConditionEntry, SmpSpecialConditionsProbe, SmpSaveWorldSelectionRoleAnalysis, SmpSaveWorldSelectionRoleAnalysisEntry,
SmpSecondaryVariantProbe, SmpSharedHeader, SmpSpecialConditionEntry, SmpSpecialConditionsProbe,
inspect_save_company_and_chairman_analysis_bytes, inspect_save_company_and_chairman_analysis_bytes,
inspect_save_company_and_chairman_analysis_file, inspect_smp_bytes, inspect_smp_file, inspect_save_company_and_chairman_analysis_file, inspect_smp_bytes, inspect_smp_file,
load_save_slice_file, load_save_slice_from_report, load_save_slice_file, load_save_slice_from_report,

View file

@ -2185,9 +2185,41 @@ pub struct SmpLoadedChairmanProfileTable {
pub struct SmpSaveScalarCandidate { pub struct SmpSaveScalarCandidate {
pub relative_offset: usize, pub relative_offset: usize,
pub relative_offset_hex: String, pub relative_offset_hex: String,
pub raw_u64: u64,
pub raw_u64_hex: String,
pub value_i64: i64,
pub value_f64: f64, pub value_f64: f64,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpSaveDwordCandidate {
pub label: String,
pub relative_offset: usize,
pub relative_offset_hex: String,
pub raw_u32: u32,
pub raw_u32_hex: String,
pub value_i32: i32,
pub value_f32: f32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveWorldSelectionRoleAnalysisEntry {
pub slot_index: usize,
pub selector_byte: u8,
pub selector_byte_hex: String,
pub role_gate_byte: u8,
pub role_gate_byte_hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveWorldSelectionRoleAnalysis {
pub selected_company_id: u32,
pub selected_chairman_profile_id: u32,
pub campaign_override_flag: u8,
pub campaign_override_flag_hex: String,
pub chairman_slots: Vec<SmpSaveWorldSelectionRoleAnalysisEntry>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SmpSaveCompanyRecordAnalysisEntry { pub struct SmpSaveCompanyRecordAnalysisEntry {
pub company_id: u32, pub company_id: u32,
@ -2211,6 +2243,10 @@ pub struct SmpSaveCompanyRecordAnalysisEntry {
pub linked_transit_latch: bool, pub linked_transit_latch: bool,
pub merger_cooldown_year: u32, pub merger_cooldown_year: u32,
pub takeover_cooldown_year: u32, pub takeover_cooldown_year: u32,
#[serde(default)]
pub scalar_dword_candidates: Vec<SmpSaveDwordCandidate>,
#[serde(default)]
pub post_capacity_dword_candidates: Vec<SmpSaveDwordCandidate>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -2235,6 +2271,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
#[serde(default)] #[serde(default)]
pub selected_chairman_profile_id: Option<u32>, pub selected_chairman_profile_id: Option<u32>,
#[serde(default)] #[serde(default)]
pub world_selection_context: Option<SmpSaveWorldSelectionRoleAnalysis>,
#[serde(default)]
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>, pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
#[serde(default)] #[serde(default)]
pub chairman_entries: Vec<SmpSaveChairmanRecordAnalysisEntry>, pub chairman_entries: Vec<SmpSaveChairmanRecordAnalysisEntry>,
@ -2758,6 +2796,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
report: &SmpInspectionReport, report: &SmpInspectionReport,
) -> Option<SmpSaveCompanyChairmanAnalysisReport> { ) -> Option<SmpSaveCompanyChairmanAnalysisReport> {
let selection_probe = report.save_world_selection_context_probe.as_ref(); 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 company_header_probe = report.save_company_collection_header_probe.as_ref(); let company_header_probe = report.save_company_collection_header_probe.as_ref();
let chairman_header_probe = report let chairman_header_probe = report
.save_chairman_profile_collection_header_probe .save_chairman_profile_collection_header_probe
@ -2840,6 +2879,18 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
&bytes, &bytes,
record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET, record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET,
)?; )?;
let scalar_dword_candidates = SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS
.iter()
.map(|(label, relative_offset)| {
build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
})
.collect::<Option<Vec<_>>>()?;
let post_capacity_dword_candidates = SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS
.iter()
.map(|(label, relative_offset)| {
build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
})
.collect::<Option<Vec<_>>>()?;
entries.push(SmpSaveCompanyRecordAnalysisEntry { entries.push(SmpSaveCompanyRecordAnalysisEntry {
company_id, company_id,
name, name,
@ -2860,6 +2911,8 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
linked_transit_latch, linked_transit_latch,
merger_cooldown_year, merger_cooldown_year,
takeover_cooldown_year, takeover_cooldown_year,
scalar_dword_candidates,
post_capacity_dword_candidates,
}); });
} }
entries entries
@ -2907,12 +2960,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS
.iter() .iter()
.map(|relative_offset| { .map(|relative_offset| {
let value_f64 = read_f64_at(&bytes, record_offset + relative_offset)?; build_save_qword_candidate(&bytes, record_offset, *relative_offset)
Some(SmpSaveScalarCandidate {
relative_offset: *relative_offset,
relative_offset_hex: format!("0x{relative_offset:x}"),
value_f64,
})
}) })
.collect::<Option<Vec<_>>>()?; .collect::<Option<Vec<_>>>()?;
entries.push(SmpSaveChairmanRecordAnalysisEntry { entries.push(SmpSaveChairmanRecordAnalysisEntry {
@ -2931,6 +2979,12 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
}; };
let mut notes = Vec::new(); let mut notes = Vec::new();
if world_selection_context.is_some() {
notes.push(
"World selection context now exports the grounded chairman-slot selector bytes and per-slot role-gate bytes from the fixed save-side 0x32c8 world block."
.to_string(),
);
}
if !company_entries.is_empty() { if !company_entries.is_empty() {
notes.push( notes.push(
"Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string(), "Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string(),
@ -2938,10 +2992,13 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
notes.push( notes.push(
"Company available_track_laying_capacity is derived from the grounded tail dword [company+0x7680], with negative values treated as the unlimited sentinel.".to_string(), "Company available_track_laying_capacity is derived from the grounded tail dword [company+0x7680], with negative values treated as the unlimited sentinel.".to_string(),
); );
notes.push(
"Company scalar_dword_candidates expose the current checked-in raw save windows around support/share-price/calendar lanes, and post_capacity_dword_candidates expose the immediate dwords after [company+0x7680] for deeper track-count and record-tail analysis.".to_string(),
);
} }
if !chairman_entries.is_empty() { if !chairman_entries.is_empty() {
notes.push( notes.push(
"Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9] for further purchasing-power analysis.".to_string(), "Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9], now including raw qword hex and signed/f64 views for further purchasing-power analysis.".to_string(),
); );
} }
@ -2954,6 +3011,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
selected_company_id: selection_probe.map(|probe| probe.selected_company_id), selected_company_id: selection_probe.map(|probe| probe.selected_company_id),
selected_chairman_profile_id: selection_probe selected_chairman_profile_id: selection_probe
.map(|probe| probe.selected_chairman_profile_id), .map(|probe| probe.selected_chairman_profile_id),
world_selection_context,
company_entries, company_entries,
chairman_entries, chairman_entries,
notes, notes,
@ -3089,6 +3147,23 @@ const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET: usize = 0x0d59;
const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56; const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56;
const SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET: usize = 0x0d7b; const SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET: usize = 0x0d7b;
const SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET: usize = 0x7680; const SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET: usize = 0x7680;
const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 7] = [
("mutable_support_scalar", 0x4f),
("young_company_support_scalar", 0x57),
("support_progress_word", 0x0d07),
("recent_per_share_subscore", 0x0d19),
("cached_share_price", 0x0d7b),
("current_issue_calendar_word", 0x16b),
("prior_issue_calendar_word", 0x173),
];
const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [(&str, usize); 6] = [
("post_capacity_word_1", 0x7684),
("post_capacity_word_2", 0x7688),
("post_capacity_word_3", 0x768c),
("post_capacity_word_4", 0x7690),
("post_capacity_word_5", 0x7694),
("post_capacity_word_6", 0x7698),
];
const SAVE_COMPANY_RECORD_START_SCAN_LIMIT: usize = 0x120; const SAVE_COMPANY_RECORD_START_SCAN_LIMIT: usize = 0x120;
const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08; const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08;
@ -3296,6 +3371,69 @@ fn parse_save_company_available_track_laying_capacity(
} }
} }
fn build_save_dword_candidate(
bytes: &[u8],
record_offset: usize,
label: &str,
relative_offset: usize,
) -> Option<SmpSaveDwordCandidate> {
let raw_u32 = read_u32_at(bytes, record_offset + relative_offset)?;
Some(SmpSaveDwordCandidate {
label: label.to_string(),
relative_offset,
relative_offset_hex: format!("0x{relative_offset:x}"),
raw_u32,
raw_u32_hex: format!("0x{raw_u32:08x}"),
value_i32: raw_u32 as i32,
value_f32: f32::from_bits(raw_u32),
})
}
fn build_save_qword_candidate(
bytes: &[u8],
record_offset: usize,
relative_offset: usize,
) -> Option<SmpSaveScalarCandidate> {
let raw_u64 = read_u64_at(bytes, record_offset + relative_offset)?;
Some(SmpSaveScalarCandidate {
relative_offset,
relative_offset_hex: format!("0x{relative_offset:x}"),
raw_u64,
raw_u64_hex: format!("0x{raw_u64:016x}"),
value_i64: raw_u64 as i64,
value_f64: f64::from_bits(raw_u64),
})
}
fn build_save_world_selection_role_analysis(
probe: &SmpSaveWorldSelectionContextProbe,
) -> SmpSaveWorldSelectionRoleAnalysis {
let chairman_slots = probe
.chairman_slot_selectors
.iter()
.copied()
.zip(probe.chairman_role_gate_bytes.iter().copied())
.enumerate()
.map(|(slot_index, (selector_byte, role_gate_byte))| {
SmpSaveWorldSelectionRoleAnalysisEntry {
slot_index,
selector_byte,
selector_byte_hex: format!("0x{selector_byte:02x}"),
role_gate_byte,
role_gate_byte_hex: format!("0x{role_gate_byte:02x}"),
}
})
.collect();
SmpSaveWorldSelectionRoleAnalysis {
selected_company_id: probe.selected_company_id,
selected_chairman_profile_id: probe.selected_chairman_profile_id,
campaign_override_flag: probe.campaign_override_flag,
campaign_override_flag_hex: probe.campaign_override_flag_hex.clone(),
chairman_slots,
}
}
fn parse_save_chairman_profile_table_probe( fn parse_save_chairman_profile_table_probe(
bytes: &[u8], bytes: &[u8],
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>, header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
@ -9986,6 +10124,13 @@ fn read_i64_at(bytes: &[u8], offset: usize) -> Option<i64> {
])) ]))
} }
fn read_u64_at(bytes: &[u8], offset: usize) -> Option<u64> {
let chunk = bytes.get(offset..offset + 8)?;
Some(u64::from_le_bytes([
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
]))
}
fn read_f32_at(bytes: &[u8], offset: usize) -> Option<f32> { fn read_f32_at(bytes: &[u8], offset: usize) -> Option<f32> {
let chunk = bytes.get(offset..offset + 4)?; let chunk = bytes.get(offset..offset + 4)?;
Some(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) Some(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
@ -14949,6 +15094,61 @@ mod tests {
assert_eq!(table.entries[1].holdings_value_total, 822000); assert_eq!(table.entries[1].holdings_value_total, 822000);
} }
#[test]
fn builds_save_world_selection_role_analysis_from_probe() {
let probe = 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: 0,
payload_offset: 0,
payload_len: 0x4f2c,
payload_len_hex: "0x4f2c".to_string(),
selected_company_id_offset: 0x21,
selected_company_id: 3,
selected_company_id_hex: "0x00000003".to_string(),
selected_chairman_profile_id_offset: 0x25,
selected_chairman_profile_id: 7,
selected_chairman_profile_id_hex: "0x00000007".to_string(),
chairman_slot_selector_offset: 0x87,
chairman_slot_selectors: vec![1, 0, 2, 0],
campaign_override_flag_offset: 0xc5,
campaign_override_flag: 1,
campaign_override_flag_hex: "0x01".to_string(),
chairman_role_gate_offset: 0x0bc3,
chairman_role_gate_bytes: vec![2, 0, 1, 0],
evidence: vec![],
};
let analysis = build_save_world_selection_role_analysis(&probe);
assert_eq!(analysis.selected_company_id, 3);
assert_eq!(analysis.selected_chairman_profile_id, 7);
assert_eq!(analysis.campaign_override_flag_hex, "0x01");
assert_eq!(analysis.chairman_slots.len(), 4);
assert_eq!(analysis.chairman_slots[0].selector_byte_hex, "0x01");
assert_eq!(analysis.chairman_slots[2].role_gate_byte_hex, "0x01");
}
#[test]
fn builds_save_candidate_views_with_raw_bits() {
let mut bytes = vec![0u8; 0x40];
bytes[0x08..0x0c].copy_from_slice(&0x3f800000u32.to_le_bytes());
bytes[0x10..0x18].copy_from_slice(&(-2458.0f64).to_le_bytes());
let dword = build_save_dword_candidate(&bytes, 0, "unit_float", 0x08)
.expect("dword candidate should build");
let qword =
build_save_qword_candidate(&bytes, 0, 0x10).expect("qword candidate should build");
assert_eq!(dword.raw_u32_hex, "0x3f800000");
assert_eq!(dword.value_i32, 1_065_353_216);
assert_eq!(dword.value_f32, 1.0);
assert_eq!(qword.raw_u64, (-2458.0f64).to_bits());
assert_eq!(qword.value_i64, (-2458.0f64).to_bits() as i64);
assert_eq!(qword.value_f64, -2458.0);
}
#[test] #[test]
fn classifies_rt3_105_post_span_bridge_variants() { fn classifies_rt3_105_post_span_bridge_variants() {
let base_trailer = SmpRuntimeTrailerBlock { let base_trailer = SmpRuntimeTrailerBlock {

View file

@ -105,7 +105,8 @@ The highest-value next passes are now:
finance/governance scalar lanes plus controller-kind reconstruction still remain conservative finance/governance scalar lanes plus controller-kind reconstruction still remain conservative
defaults until their raw offsets are pinned more strongly; the offline analysis command defaults until their raw offsets are pinned more strongly; the offline analysis command
`runtime inspect-save-company-chairman <save.gms>` now dumps those remaining raw record `runtime inspect-save-company-chairman <save.gms>` now dumps those remaining raw record
candidates directly from the rehosted parser candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate
context, company dword candidate windows, and richer chairman qword cache views
- a checked-in `EventEffects` export now exists at - a checked-in `EventEffects` export now exists at
`artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer `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` now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`

View file

@ -65,8 +65,10 @@ Implemented today:
collections now provide save-native roster entries and `observed_entry_count`; raw company debt 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, from the bond table and raw company track-laying capacity from the record tail are grounded too,
and `runtime inspect-save-company-chairman <save.gms>` now exposes the remaining raw and `runtime inspect-save-company-chairman <save.gms>` now exposes the remaining raw
company/chairman scalar candidates directly from the rehosted parser; the remaining raw-save company/chairman scalar candidates directly from the rehosted parser, including fixed-world
boundary is company-finance/governance scalar depth plus controller-kind closure, not roster absence chairman slot / role-gate context, company dword candidate windows, and richer chairman qword
cache views; the remaining raw-save boundary is company-finance/governance scalar depth plus
controller-kind closure, not roster absence
- a checked-in `EventEffects` export now exists too at - a checked-in `EventEffects` export now exists too at
`artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer `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` now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`