Carry company stat-band roots into runtime state

This commit is contained in:
Jan Petykiewicz 2026-04-17 20:42:28 -07:00
commit 1e0f88bd62
9 changed files with 180 additions and 15 deletions

View file

@ -51,8 +51,9 @@ math. The next shared company-side slice is now rehosted too: save-native compan
flow into a typed company market/cache map on runtime service state, carrying outstanding shares,
saved support/share-price/cache words, chairman salary lanes, calendar words, and connection
latches for each live company. That map now appears in runtime summaries and save-slice exports,
so later company stat-family / finance readers can build on owned state instead of another round
of single-field save-offset guesses. A checked-in
and it now also carries the first grounded stat-band root windows at `[company+0x0cfb]`,
`[company+0x0d7f]`, and `[company+0x1c47]`, so later company stat-family / finance readers can
build on owned state instead of another round of single-field save-offset guesses. 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

View file

@ -96,6 +96,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub selected_company_mutable_support_scalar_value_f32_text: Option<String>,
#[serde(default)]
pub selected_company_stat_band_root_0cfb_count: Option<usize>,
#[serde(default)]
pub selected_company_stat_band_root_0d7f_count: Option<usize>,
#[serde(default)]
pub selected_company_stat_band_root_1c47_count: Option<usize>,
#[serde(default)]
pub player_count: Option<usize>,
#[serde(default)]
pub chairman_profile_count: Option<usize>,
@ -609,6 +615,30 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(count) = self.selected_company_stat_band_root_0cfb_count {
if actual.selected_company_stat_band_root_0cfb_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_0cfb_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_0cfb_count
));
}
}
if let Some(count) = self.selected_company_stat_band_root_0d7f_count {
if actual.selected_company_stat_band_root_0d7f_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_0d7f_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_0d7f_count
));
}
}
if let Some(count) = self.selected_company_stat_band_root_1c47_count {
if actual.selected_company_stat_band_root_1c47_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_1c47_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_1c47_count
));
}
}
if let Some(count) = self.player_count {
if actual.player_count != count {
mismatches.push(format!(

View file

@ -5057,6 +5057,9 @@ mod tests {
prior_issue_calendar_word: 4,
city_connection_latch: true,
linked_transit_latch: false,
stat_band_root_0cfb_candidates: Vec::new(),
stat_band_root_0d7f_candidates: Vec::new(),
stat_band_root_1c47_candidates: Vec::new(),
}),
},
crate::SmpLoadedCompanyRosterEntry {
@ -5100,6 +5103,9 @@ mod tests {
prior_issue_calendar_word: 2,
city_connection_latch: false,
linked_transit_latch: true,
stat_band_root_0cfb_candidates: Vec::new(),
stat_band_root_0d7f_candidates: Vec::new(),
stat_band_root_1c47_candidates: Vec::new(),
}),
},
],
@ -6386,6 +6392,9 @@ mod tests {
prior_issue_calendar_word: 3,
city_connection_latch: false,
linked_transit_latch: true,
stat_band_root_0cfb_candidates: Vec::new(),
stat_band_root_0d7f_candidates: Vec::new(),
stat_band_root_1c47_candidates: Vec::new(),
},
)]),
..RuntimeServiceState::default()

View file

@ -48,16 +48,17 @@ pub use runtime::{
RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile,
RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope,
RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric,
RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer,
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeTerritory, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain,
RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldRestoreState,
RuntimeCompanyStatBandCandidate, RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess,
RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition, RuntimeConditionComparator,
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry,
RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary,
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary,
RuntimePackedEventTextBandSummary, RuntimePlayer, RuntimePlayerConditionTestScope,
RuntimePlayerTarget, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
RuntimeTerritory, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate,
RuntimeWorldRestoreState,
};
pub use smp::{
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,

View file

@ -84,6 +84,12 @@ pub struct RuntimeCompanyMarketState {
pub city_connection_latch: bool,
#[serde(default)]
pub linked_transit_latch: bool,
#[serde(default)]
pub stat_band_root_0cfb_candidates: Vec<RuntimeCompanyStatBandCandidate>,
#[serde(default)]
pub stat_band_root_0d7f_candidates: Vec<RuntimeCompanyStatBandCandidate>,
#[serde(default)]
pub stat_band_root_1c47_candidates: Vec<RuntimeCompanyStatBandCandidate>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
@ -871,6 +877,17 @@ pub struct RuntimeWorldFinanceNeighborhoodCandidate {
pub value_f32_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCompanyStatBandCandidate {
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_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeWorldRestoreState {
#[serde(default)]

View file

@ -3754,6 +3754,27 @@ fn parse_save_company_roster_probe(
bytes,
record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET,
)?;
let stat_band_root_0cfb_candidates =
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_CANDIDATE_FIELDS
.iter()
.map(|(label, relative_offset)| {
build_save_dword_candidate(bytes, record_offset, label, *relative_offset)
})
.collect::<Option<Vec<_>>>()?;
let stat_band_root_0d7f_candidates =
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_CANDIDATE_FIELDS
.iter()
.map(|(label, relative_offset)| {
build_save_dword_candidate(bytes, record_offset, label, *relative_offset)
})
.collect::<Option<Vec<_>>>()?;
let stat_band_root_1c47_candidates =
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_CANDIDATE_FIELDS
.iter()
.map(|(label, relative_offset)| {
build_save_dword_candidate(bytes, record_offset, label, *relative_offset)
})
.collect::<Option<Vec<_>>>()?;
entries.push(SmpLoadedCompanyRosterEntry {
company_id,
active,
@ -3788,6 +3809,18 @@ fn parse_save_company_roster_probe(
prior_issue_calendar_word,
city_connection_latch,
linked_transit_latch,
stat_band_root_0cfb_candidates: stat_band_root_0cfb_candidates
.iter()
.map(runtime_company_stat_band_candidate_from_save)
.collect(),
stat_band_root_0d7f_candidates: stat_band_root_0d7f_candidates
.iter()
.map(runtime_company_stat_band_candidate_from_save)
.collect(),
stat_band_root_1c47_candidates: stat_band_root_1c47_candidates
.iter()
.map(runtime_company_stat_band_candidate_from_save)
.collect(),
}),
});
}
@ -3801,6 +3834,20 @@ fn parse_save_company_roster_probe(
})
}
fn runtime_company_stat_band_candidate_from_save(
candidate: &SmpSaveDwordCandidate,
) -> crate::RuntimeCompanyStatBandCandidate {
crate::RuntimeCompanyStatBandCandidate {
label: candidate.label.clone(),
relative_offset: candidate.relative_offset,
relative_offset_hex: candidate.relative_offset_hex.clone(),
raw_u32: candidate.raw_u32,
raw_u32_hex: candidate.raw_u32_hex.clone(),
value_i32: candidate.value_i32,
value_f32_text: format!("{:.6}", candidate.value_f32),
}
}
fn detect_save_company_record_start_offset(
bytes: &[u8],
header_probe: &SmpSaveTaggedCollectionHeaderProbe,

View file

@ -49,6 +49,9 @@ pub struct RuntimeSummary {
pub selected_company_outstanding_shares: Option<u32>,
pub selected_company_cached_share_price_value_f32_text: Option<String>,
pub selected_company_mutable_support_scalar_value_f32_text: Option<String>,
pub selected_company_stat_band_root_0cfb_count: usize,
pub selected_company_stat_band_root_0d7f_count: usize,
pub selected_company_stat_band_root_1c47_count: usize,
pub selected_company_last_dividend_year: Option<u32>,
pub selected_company_chairman_bonus_year: Option<u32>,
pub selected_company_chairman_bonus_amount: Option<i32>,
@ -249,6 +252,15 @@ impl RuntimeSummary {
.map(|market_state| {
raw_u32_to_f32_text(market_state.mutable_support_scalar_raw_u32)
}),
selected_company_stat_band_root_0cfb_count: selected_company_market_state
.map(|market_state| market_state.stat_band_root_0cfb_candidates.len())
.unwrap_or(0),
selected_company_stat_band_root_0d7f_count: selected_company_market_state
.map(|market_state| market_state.stat_band_root_0d7f_candidates.len())
.unwrap_or(0),
selected_company_stat_band_root_1c47_count: selected_company_market_state
.map(|market_state| market_state.stat_band_root_1c47_candidates.len())
.unwrap_or(0),
selected_company_last_dividend_year: selected_company_market_state
.map(|market_state| market_state.last_dividend_year)
.filter(|year| *year != 0),
@ -1890,6 +1902,48 @@ mod tests {
prior_issue_calendar_word: 4,
city_connection_latch: true,
linked_transit_latch: false,
stat_band_root_0cfb_candidates: vec![
crate::RuntimeCompanyStatBandCandidate {
label: "stat_band_0cfb_word_1".to_string(),
relative_offset: 0x0cfb,
relative_offset_hex: "0xcfb".to_string(),
raw_u32: 1,
raw_u32_hex: "0x00000001".to_string(),
value_i32: 1,
value_f32_text: "0.000000".to_string(),
},
crate::RuntimeCompanyStatBandCandidate {
label: "stat_band_0cfb_word_2".to_string(),
relative_offset: 0x0cff,
relative_offset_hex: "0xcff".to_string(),
raw_u32: 2,
raw_u32_hex: "0x00000002".to_string(),
value_i32: 2,
value_f32_text: "0.000000".to_string(),
},
],
stat_band_root_0d7f_candidates: vec![
crate::RuntimeCompanyStatBandCandidate {
label: "stat_band_0d7f_word_1".to_string(),
relative_offset: 0x0d7f,
relative_offset_hex: "0xd7f".to_string(),
raw_u32: 3,
raw_u32_hex: "0x00000003".to_string(),
value_i32: 3,
value_f32_text: "0.000000".to_string(),
},
],
stat_band_root_1c47_candidates: vec![
crate::RuntimeCompanyStatBandCandidate {
label: "stat_band_1c47_word_1".to_string(),
relative_offset: 0x1c47,
relative_offset_hex: "0x1c47".trim_start_matches("0x").to_string(),
raw_u32: 4,
raw_u32_hex: "0x00000004".to_string(),
value_i32: 4,
value_f32_text: "0.000000".to_string(),
},
],
},
)]),
..RuntimeServiceState::default()
@ -1907,6 +1961,9 @@ mod tests {
summary.selected_company_mutable_support_scalar_value_f32_text,
Some("1.000000".to_string())
);
assert_eq!(summary.selected_company_stat_band_root_0cfb_count, 2);
assert_eq!(summary.selected_company_stat_band_root_0d7f_count, 1);
assert_eq!(summary.selected_company_stat_band_root_1c47_count, 1);
assert_eq!(summary.selected_company_last_dividend_year, Some(1841));
assert_eq!(summary.selected_company_chairman_bonus_year, Some(1842));
assert_eq!(summary.selected_company_chairman_bonus_amount, Some(750));

View file

@ -112,8 +112,9 @@ The highest-value next passes are now:
tuning band, derived holdings-at-share-price and cached purchasing-power totals,
context, company dword candidate windows, and richer chairman qword cache views; the current
rehosted company-side owner state now also includes a typed market/cache map carrying saved
outstanding-shares, support/share-price/cache words, salary lanes, calendar words, and
connection latches for each live company, so later finance/stat-family readers can attach to
outstanding-shares, support/share-price/cache words, salary lanes, calendar words, connection
latches, and the first grounded stat-band root windows at `[company+0x0cfb]`, `[company+0x0d7f]`,
and `[company+0x1c47]` for each live company, so later finance/stat-family readers can attach to
owned runtime data instead of one more guessed save offset
- 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

View file

@ -197,7 +197,9 @@ later tranche once stronger evidence exists, but the current project rule is exp
rehosting shared owner state and reader/setter families first, and only guess at one more leaf
field when that richer owning-state path is blocked. Richer runtime ownership should still be added
where later descriptor, stat-family, or simulation work needs more than the current event-owned
roster.
roster. The current owned company-side roster now includes not just the market/cache lanes but also
the first grounded stat-band root windows at `[company+0x0cfb]`, `[company+0x0d7f]`, and
`[company+0x1c47]`, so later finance readers can target saved owner state directly.
## Why This Boundary