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
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
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
`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

View file

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

View file

@ -2185,9 +2185,41 @@ pub struct SmpLoadedChairmanProfileTable {
pub struct SmpSaveScalarCandidate {
pub relative_offset: usize,
pub relative_offset_hex: String,
pub raw_u64: u64,
pub raw_u64_hex: String,
pub value_i64: i64,
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)]
pub struct SmpSaveCompanyRecordAnalysisEntry {
pub company_id: u32,
@ -2211,6 +2243,10 @@ pub struct SmpSaveCompanyRecordAnalysisEntry {
pub linked_transit_latch: bool,
pub merger_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)]
@ -2235,6 +2271,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
#[serde(default)]
pub selected_chairman_profile_id: Option<u32>,
#[serde(default)]
pub world_selection_context: Option<SmpSaveWorldSelectionRoleAnalysis>,
#[serde(default)]
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
#[serde(default)]
pub chairman_entries: Vec<SmpSaveChairmanRecordAnalysisEntry>,
@ -2758,6 +2796,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
report: &SmpInspectionReport,
) -> 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 company_header_probe = report.save_company_collection_header_probe.as_ref();
let chairman_header_probe = report
.save_chairman_profile_collection_header_probe
@ -2840,6 +2879,18 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
&bytes,
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 {
company_id,
name,
@ -2860,6 +2911,8 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
linked_transit_latch,
merger_cooldown_year,
takeover_cooldown_year,
scalar_dword_candidates,
post_capacity_dword_candidates,
});
}
entries
@ -2907,12 +2960,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS
.iter()
.map(|relative_offset| {
let value_f64 = read_f64_at(&bytes, record_offset + relative_offset)?;
Some(SmpSaveScalarCandidate {
relative_offset: *relative_offset,
relative_offset_hex: format!("0x{relative_offset:x}"),
value_f64,
})
build_save_qword_candidate(&bytes, record_offset, *relative_offset)
})
.collect::<Option<Vec<_>>>()?;
entries.push(SmpSaveChairmanRecordAnalysisEntry {
@ -2931,6 +2979,12 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
};
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() {
notes.push(
"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(
"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() {
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_chairman_profile_id: selection_probe
.map(|probe| probe.selected_chairman_profile_id),
world_selection_context,
company_entries,
chairman_entries,
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_CACHED_SHARE_PRICE_OFFSET: usize = 0x0d7b;
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_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(
bytes: &[u8],
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> {
let chunk = bytes.get(offset..offset + 4)?;
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);
}
#[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]
fn classifies_rt3_105_post_span_bridge_variants() {
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
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
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
`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`

View file

@ -65,8 +65,10 @@ Implemented today:
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 `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
boundary is company-finance/governance scalar depth plus controller-kind closure, not roster absence
company/chairman scalar candidates directly from the rehosted parser, including fixed-world
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
`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`