Rehost save-native company market cache state

This commit is contained in:
Jan Petykiewicz 2026-04-17 18:28:53 -07:00
commit 5198f80cd9
9 changed files with 630 additions and 79 deletions

View file

@ -46,7 +46,12 @@ holdings-at-share-price / cached purchasing-power comparisons. The same fixed `0
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
math. The next shared company-side slice is now rehosted too: save-native company direct records
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
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

@ -84,6 +84,14 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub active_company_count: Option<usize>,
#[serde(default)]
pub company_market_state_owner_count: Option<usize>,
#[serde(default)]
pub selected_company_outstanding_shares: Option<u32>,
#[serde(default)]
pub selected_company_cached_share_price_value_f32_text: Option<String>,
#[serde(default)]
pub selected_company_mutable_support_scalar_value_f32_text: Option<String>,
#[serde(default)]
pub player_count: Option<usize>,
#[serde(default)]
pub chairman_profile_count: Option<usize>,
@ -541,6 +549,46 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(count) = self.company_market_state_owner_count {
if actual.company_market_state_owner_count != count {
mismatches.push(format!(
"company_market_state_owner_count mismatch: expected {count}, got {}",
actual.company_market_state_owner_count
));
}
}
if let Some(value) = self.selected_company_outstanding_shares {
if actual.selected_company_outstanding_shares != Some(value) {
mismatches.push(format!(
"selected_company_outstanding_shares mismatch: expected {value}, got {:?}",
actual.selected_company_outstanding_shares
));
}
}
if let Some(value) = &self.selected_company_cached_share_price_value_f32_text {
if actual
.selected_company_cached_share_price_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"selected_company_cached_share_price_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.selected_company_cached_share_price_value_f32_text
));
}
}
if let Some(value) = &self.selected_company_mutable_support_scalar_value_f32_text {
if actual
.selected_company_mutable_support_scalar_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"selected_company_mutable_support_scalar_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.selected_company_mutable_support_scalar_value_f32_text
));
}
}
if let Some(count) = self.player_count {
if actual.player_count != count {
mismatches.push(format!(

View file

@ -7,9 +7,9 @@ use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapsh
use crate::{
CalendarPoint, RuntimeCargoCatalogEntry, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget,
RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany,
RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeCondition, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketState,
RuntimeCompanyTarget, RuntimeCondition, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary,
@ -99,6 +99,8 @@ struct SaveSliceProjection {
has_company_projection: bool,
has_company_selection_override: bool,
selected_company_id: Option<u32>,
company_market_state: BTreeMap<u32, RuntimeCompanyMarketState>,
has_company_market_projection: bool,
chairman_profiles: Vec<RuntimeChairmanProfile>,
has_chairman_projection: bool,
has_chairman_selection_override: bool,
@ -308,7 +310,10 @@ pub fn project_save_slice_to_runtime_state_import(
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: projection.world_scalar_overrides,
special_conditions: projection.special_conditions,
service_state: RuntimeServiceState::default(),
service_state: RuntimeServiceState {
company_market_state: projection.company_market_state,
..RuntimeServiceState::default()
},
};
state.validate()?;
@ -410,7 +415,14 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
territory_runtime_variables: base_state.territory_runtime_variables.clone(),
world_scalar_overrides: base_state.world_scalar_overrides.clone(),
special_conditions: projection.special_conditions,
service_state: base_state.service_state.clone(),
service_state: RuntimeServiceState {
company_market_state: if projection.has_company_market_projection {
projection.company_market_state
} else {
base_state.service_state.company_market_state.clone()
},
..base_state.service_state.clone()
},
};
state.validate()?;
@ -915,64 +927,88 @@ fn project_save_slice_components(
None
};
let (companies, has_company_projection, has_company_selection_override, selected_company_id) =
if let Some(roster) = &save_slice.company_roster {
let (
companies,
has_company_projection,
has_company_selection_override,
selected_company_id,
company_market_state,
has_company_market_projection,
) = if let Some(roster) = &save_slice.company_roster {
metadata.insert(
"save_slice.company_roster_source_kind".to_string(),
roster.source_kind.clone(),
);
metadata.insert(
"save_slice.company_roster_semantic_family".to_string(),
roster.semantic_family.clone(),
);
metadata.insert(
"save_slice.company_roster_entry_count".to_string(),
roster.observed_entry_count.to_string(),
);
let market_state = roster
.entries
.iter()
.filter_map(|entry| {
entry
.market_state
.as_ref()
.map(|state| (entry.company_id, state.clone()))
})
.collect::<BTreeMap<_, _>>();
metadata.insert(
"save_slice.company_market_state_owner_count".to_string(),
market_state.len().to_string(),
);
if let Some(selected_company_id) = roster.selected_company_id {
metadata.insert(
"save_slice.company_roster_source_kind".to_string(),
roster.source_kind.clone(),
"save_slice.selected_company_id".to_string(),
selected_company_id.to_string(),
);
metadata.insert(
"save_slice.company_roster_semantic_family".to_string(),
roster.semantic_family.clone(),
);
metadata.insert(
"save_slice.company_roster_entry_count".to_string(),
roster.observed_entry_count.to_string(),
);
if let Some(selected_company_id) = roster.selected_company_id {
metadata.insert(
"save_slice.selected_company_id".to_string(),
selected_company_id.to_string(),
);
}
if roster.entries.is_empty() {
(
Vec::new(),
false,
roster.selected_company_id.is_some(),
roster.selected_company_id,
)
} else {
(
roster
.entries
.iter()
.map(|entry| RuntimeCompany {
company_id: entry.company_id,
current_cash: entry.current_cash,
debt: entry.debt,
credit_rating_score: entry.credit_rating_score,
prime_rate: entry.prime_rate,
active: entry.active,
available_track_laying_capacity: entry.available_track_laying_capacity,
controller_kind: entry.controller_kind,
linked_chairman_profile_id: entry.linked_chairman_profile_id,
book_value_per_share: entry.book_value_per_share,
investor_confidence: entry.investor_confidence,
management_attitude: entry.management_attitude,
takeover_cooldown_year: entry.takeover_cooldown_year,
merger_cooldown_year: entry.merger_cooldown_year,
track_piece_counts: entry.track_piece_counts,
})
.collect::<Vec<_>>(),
true,
roster.selected_company_id.is_some(),
roster.selected_company_id,
)
}
}
if roster.entries.is_empty() {
(
Vec::new(),
false,
roster.selected_company_id.is_some(),
roster.selected_company_id,
BTreeMap::new(),
false,
)
} else {
(Vec::new(), false, false, None)
};
(
roster
.entries
.iter()
.map(|entry| RuntimeCompany {
company_id: entry.company_id,
current_cash: entry.current_cash,
debt: entry.debt,
credit_rating_score: entry.credit_rating_score,
prime_rate: entry.prime_rate,
active: entry.active,
available_track_laying_capacity: entry.available_track_laying_capacity,
controller_kind: entry.controller_kind,
linked_chairman_profile_id: entry.linked_chairman_profile_id,
book_value_per_share: entry.book_value_per_share,
investor_confidence: entry.investor_confidence,
management_attitude: entry.management_attitude,
takeover_cooldown_year: entry.takeover_cooldown_year,
merger_cooldown_year: entry.merger_cooldown_year,
track_piece_counts: entry.track_piece_counts,
})
.collect::<Vec<_>>(),
true,
roster.selected_company_id.is_some(),
roster.selected_company_id,
market_state,
true,
)
}
} else {
(Vec::new(), false, false, None, BTreeMap::new(), false)
};
let (
chairman_profiles,
@ -1112,6 +1148,8 @@ fn project_save_slice_components(
has_company_projection,
has_company_selection_override,
selected_company_id,
company_market_state,
has_company_market_projection,
chairman_profiles,
has_chairman_projection,
has_chairman_selection_override,
@ -4952,6 +4990,22 @@ mod tests {
management_attitude: 58,
takeover_cooldown_year: Some(1839),
merger_cooldown_year: Some(1838),
market_state: Some(crate::RuntimeCompanyMarketState {
outstanding_shares: 20_000,
mutable_support_scalar_raw_u32: 0x3f99999a,
young_company_support_scalar_raw_u32: 0x42700000,
support_progress_word: 12,
recent_per_share_subscore_raw_u32: 0x420c0000,
cached_share_price_raw_u32: 0x42200000,
chairman_salary_baseline: 24,
chairman_salary_current: 30,
founding_year: 1831,
last_bankruptcy_year: 0,
current_issue_calendar_word: 5,
prior_issue_calendar_word: 4,
city_connection_latch: true,
linked_transit_latch: false,
}),
},
crate::SmpLoadedCompanyRosterEntry {
company_id: 2,
@ -4976,6 +5030,22 @@ mod tests {
management_attitude: 31,
takeover_cooldown_year: None,
merger_cooldown_year: None,
market_state: Some(crate::RuntimeCompanyMarketState {
outstanding_shares: 18_000,
mutable_support_scalar_raw_u32: 0x3f4ccccd,
young_company_support_scalar_raw_u32: 0x42580000,
support_progress_word: 9,
recent_per_share_subscore_raw_u32: 0x41f00000,
cached_share_price_raw_u32: 0x41f80000,
chairman_salary_baseline: 20,
chairman_salary_current: 22,
founding_year: 1833,
last_bankruptcy_year: 0,
current_issue_calendar_word: 3,
prior_issue_calendar_word: 2,
city_connection_latch: false,
linked_transit_latch: true,
}),
},
],
}
@ -6073,6 +6143,16 @@ mod tests {
assert_eq!(import.state.selected_chairman_profile_id, Some(1));
assert_eq!(import.state.companies[0].book_value_per_share, 2620);
assert_eq!(import.state.chairman_profiles[0].current_cash, 500);
assert_eq!(import.state.service_state.company_market_state.len(), 2);
assert_eq!(
import
.state
.service_state
.company_market_state
.get(&1)
.map(|state| state.cached_share_price_raw_u32),
Some(0x42200000)
);
}
#[test]
@ -6149,6 +6229,15 @@ mod tests {
assert_eq!(import.state.chairman_profiles.len(), 2);
assert_eq!(import.state.selected_chairman_profile_id, Some(1));
assert_eq!(import.state.territories, base_state.territories);
assert_eq!(
import
.state
.service_state
.company_market_state
.get(&2)
.map(|state| state.linked_transit_latch),
Some(true)
);
}
#[test]
@ -6216,6 +6305,28 @@ mod tests {
},
],
selected_chairman_profile_id: Some(9),
service_state: RuntimeServiceState {
company_market_state: BTreeMap::from([(
42,
crate::RuntimeCompanyMarketState {
outstanding_shares: 30_000,
mutable_support_scalar_raw_u32: 0x3f19999a,
young_company_support_scalar_raw_u32: 0x42580000,
support_progress_word: 8,
recent_per_share_subscore_raw_u32: 0x42000000,
cached_share_price_raw_u32: 0x42180000,
chairman_salary_baseline: 21,
chairman_salary_current: 24,
founding_year: 1834,
last_bankruptcy_year: 0,
current_issue_calendar_word: 4,
prior_issue_calendar_word: 3,
city_connection_latch: false,
linked_transit_latch: true,
},
)]),
..RuntimeServiceState::default()
},
..state()
};
let save_slice = SmpLoadedSaveSlice {
@ -6263,6 +6374,10 @@ mod tests {
assert_eq!(import.state.selected_company_id, Some(1));
assert_eq!(import.state.chairman_profiles, base_state.chairman_profiles);
assert_eq!(import.state.selected_chairman_profile_id, Some(1));
assert_eq!(
import.state.service_state.company_market_state,
base_state.service_state.company_market_state
);
}
#[test]
@ -13371,6 +13486,7 @@ mod tests {
trigger_dispatch_counts: BTreeMap::new(),
total_event_record_services: 4,
dirty_rerun_count: 2,
company_market_state: BTreeMap::new(),
},
};
let save_slice = SmpLoadedSaveSlice {

View file

@ -47,10 +47,10 @@ pub use runtime::{
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget,
RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile,
RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyConditionTestScope,
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget,
RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric,
RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer,

View file

@ -48,6 +48,38 @@ pub struct RuntimeCompany {
pub track_piece_counts: RuntimeTrackPieceCounts,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeCompanyMarketState {
#[serde(default)]
pub outstanding_shares: u32,
#[serde(default)]
pub mutable_support_scalar_raw_u32: u32,
#[serde(default)]
pub young_company_support_scalar_raw_u32: u32,
#[serde(default)]
pub support_progress_word: u32,
#[serde(default)]
pub recent_per_share_subscore_raw_u32: u32,
#[serde(default)]
pub cached_share_price_raw_u32: u32,
#[serde(default)]
pub chairman_salary_baseline: u32,
#[serde(default)]
pub chairman_salary_current: u32,
#[serde(default)]
pub founding_year: u32,
#[serde(default)]
pub last_bankruptcy_year: u32,
#[serde(default)]
pub current_issue_calendar_word: u32,
#[serde(default)]
pub prior_issue_calendar_word: u32,
#[serde(default)]
pub city_connection_latch: bool,
#[serde(default)]
pub linked_transit_latch: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RuntimeTrackPieceCounts {
#[serde(default)]
@ -798,6 +830,8 @@ pub struct RuntimeServiceState {
pub total_event_record_services: u64,
#[serde(default)]
pub dirty_rerun_count: u64,
#[serde(default)]
pub company_market_state: BTreeMap<u32, RuntimeCompanyMarketState>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
@ -1629,6 +1663,14 @@ impl RuntimeState {
}
}
}
for company_id in self.service_state.company_market_state.keys() {
if !seen_company_ids.contains(company_id) {
return Err(format!(
"service_state.company_market_state references unknown company_id {}",
company_id
));
}
}
for (player_id, vars) in &self.player_runtime_variables {
if !seen_player_ids.contains(player_id) {
return Err(format!(

View file

@ -9,10 +9,10 @@ use sha2::{Digest, Sha256};
use crate::{
RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget,
RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope,
RuntimeCompanyControllerKind, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric,
RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect,
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimePlayerTarget,
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
};
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
@ -2196,6 +2196,8 @@ pub struct SmpLoadedCompanyRosterEntry {
pub takeover_cooldown_year: Option<u32>,
#[serde(default)]
pub merger_cooldown_year: Option<u32>,
#[serde(default)]
pub market_state: Option<RuntimeCompanyMarketState>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -3329,20 +3331,36 @@ const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET: usize = 0x14f;
const SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET: usize = 0x15f;
const SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET: usize = 0x157;
const SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163;
const SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET: usize = 0x16b;
const SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET: usize = 0x173;
const SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET: usize = 0x289;
const SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18;
const SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET: usize = 0x0d07;
const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET: usize = 0x0d59;
const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56;
const SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET: usize = 0x0d19;
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),
(
"support_progress_word",
SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET,
),
(
"recent_per_share_subscore",
SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET,
),
("cached_share_price", 0x0d7b),
("current_issue_calendar_word", 0x16b),
("prior_issue_calendar_word", 0x173),
(
"current_issue_calendar_word",
SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET,
),
(
"prior_issue_calendar_word",
SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET,
),
];
const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [(&str, usize); 6] = [
("post_capacity_word_1", 0x7684),
@ -3398,9 +3416,65 @@ fn parse_save_company_roster_probe(
bytes,
record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET,
)?;
let outstanding_shares = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET,
)?;
let debt = parse_save_company_total_debt(bytes, record_offset)?;
let available_track_laying_capacity =
parse_save_company_available_track_laying_capacity(bytes, record_offset)?;
let mutable_support_scalar_raw_u32 = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET,
)?;
let young_company_support_scalar_raw_u32 = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET,
)?;
let support_progress_word = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET,
)?;
let recent_per_share_subscore_raw_u32 = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET,
)?;
let cached_share_price_raw_u32 = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET,
)?;
let chairman_salary_baseline = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET,
)?;
let chairman_salary_current = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET,
)?;
let founding_year = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET,
)?;
let last_bankruptcy_year = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET,
)?;
let current_issue_calendar_word = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET,
)?;
let prior_issue_calendar_word = read_u32_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET,
)?;
let city_connection_latch = read_u8_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET,
)? != 0;
let linked_transit_latch = read_u8_at(
bytes,
record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET,
)? != 0;
let merger_cooldown_year = parse_nonzero_u32(
bytes,
record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET,
@ -3425,6 +3499,22 @@ fn parse_save_company_roster_probe(
management_attitude: 0,
takeover_cooldown_year,
merger_cooldown_year,
market_state: Some(RuntimeCompanyMarketState {
outstanding_shares,
mutable_support_scalar_raw_u32,
young_company_support_scalar_raw_u32,
support_progress_word,
recent_per_share_subscore_raw_u32,
cached_share_price_raw_u32,
chairman_salary_baseline,
chairman_salary_current,
founding_year,
last_bankruptcy_year,
current_issue_calendar_word,
prior_issue_calendar_word,
city_connection_latch,
linked_transit_latch,
}),
});
}
@ -15386,7 +15476,31 @@ mod tests {
.copy_from_slice(&0x000061aau32.to_le_bytes());
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000061abu32.to_le_bytes());
for (index, (name, linked, merger, takeover, bond_count, _debt, track_capacity)) in [
for (
index,
(
name,
linked,
merger,
takeover,
bond_count,
_debt,
track_capacity,
mutable_support_scalar_raw_u32,
young_company_support_scalar_raw_u32,
support_progress_word,
recent_per_share_subscore_raw_u32,
cached_share_price_raw_u32,
chairman_salary_baseline,
chairman_salary_current,
founding_year,
last_bankruptcy_year,
current_issue_calendar_word,
prior_issue_calendar_word,
city_connection_latch,
linked_transit_latch,
),
) in [
(
"Company One",
1u32,
@ -15395,8 +15509,42 @@ mod tests {
2u8,
1_000_000u32,
Some(603i32),
0x3f800000u32,
0x42340000u32,
17u32,
0x41f00000u32,
0x426c0000u32,
24u32,
31u32,
1842u32,
1851u32,
7u32,
6u32,
true,
false,
),
(
"Company Two",
2u32,
0u32,
1871u32,
1u8,
500_000u32,
None,
0x40000000u32,
0x42700000u32,
33u32,
0x42000000u32,
0x42780000u32,
28u32,
36u32,
1845u32,
0u32,
3u32,
2u32,
false,
true,
),
("Company Two", 2u32, 0u32, 1871u32, 1u8, 500_000u32, None),
]
.into_iter()
.enumerate()
@ -15412,6 +15560,12 @@ mod tests {
bytes[record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET] = 1;
bytes[record_offset + 0x47..record_offset + 0x4b]
.copy_from_slice(&20000u32.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET
..record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET + 4]
.copy_from_slice(&mutable_support_scalar_raw_u32.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET
..record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET + 4]
.copy_from_slice(&young_company_support_scalar_raw_u32.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET] = bond_count;
for slot_index in 0..bond_count as usize {
let slot_offset = record_offset
@ -15432,6 +15586,37 @@ mod tests {
bytes[record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET
..record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET + 4]
.copy_from_slice(&raw_capacity.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET
..record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET + 4]
.copy_from_slice(&support_progress_word.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET
..record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET + 4]
.copy_from_slice(&recent_per_share_subscore_raw_u32.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET
..record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET + 4]
.copy_from_slice(&cached_share_price_raw_u32.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET
..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET + 4]
.copy_from_slice(&chairman_salary_baseline.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET
..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET + 4]
.copy_from_slice(&chairman_salary_current.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET
..record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET + 4]
.copy_from_slice(&founding_year.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET
..record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET + 4]
.copy_from_slice(&last_bankruptcy_year.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET
..record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET + 4]
.copy_from_slice(&current_issue_calendar_word.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET
..record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET + 4]
.copy_from_slice(&prior_issue_calendar_word.to_le_bytes());
bytes[record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET] =
u8::from(city_connection_latch);
bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET] =
u8::from(linked_transit_latch);
}
let header_probe = parse_save_company_collection_header_probe(
@ -15481,11 +15666,40 @@ mod tests {
assert_eq!(roster.entries[0].debt, 1_000_000);
assert_eq!(roster.entries[0].available_track_laying_capacity, Some(603));
assert_eq!(roster.entries[0].merger_cooldown_year, Some(1862));
let market_state = roster.entries[0]
.market_state
.as_ref()
.expect("company market state should load");
assert_eq!(market_state.outstanding_shares, 20_000);
assert_eq!(market_state.mutable_support_scalar_raw_u32, 0x3f800000);
assert_eq!(
market_state.young_company_support_scalar_raw_u32,
0x42340000
);
assert_eq!(market_state.support_progress_word, 17);
assert_eq!(market_state.recent_per_share_subscore_raw_u32, 0x41f00000);
assert_eq!(market_state.cached_share_price_raw_u32, 0x426c0000);
assert_eq!(market_state.chairman_salary_baseline, 24);
assert_eq!(market_state.chairman_salary_current, 31);
assert_eq!(market_state.founding_year, 1842);
assert_eq!(market_state.last_bankruptcy_year, 1851);
assert_eq!(market_state.current_issue_calendar_word, 7);
assert_eq!(market_state.prior_issue_calendar_word, 6);
assert!(market_state.city_connection_latch);
assert!(!market_state.linked_transit_latch);
assert_eq!(roster.entries[1].company_id, 2);
assert_eq!(roster.entries[1].linked_chairman_profile_id, Some(2));
assert_eq!(roster.entries[1].debt, 500_000);
assert_eq!(roster.entries[1].available_track_laying_capacity, None);
assert_eq!(roster.entries[1].takeover_cooldown_year, Some(1871));
let second_market_state = roster.entries[1]
.market_state
.as_ref()
.expect("second company market state should load");
assert_eq!(second_market_state.current_issue_calendar_word, 3);
assert_eq!(second_market_state.prior_issue_calendar_word, 2);
assert!(!second_market_state.city_connection_latch);
assert!(second_market_state.linked_transit_latch);
}
#[test]

View file

@ -2,6 +2,10 @@ use serde::{Deserialize, Serialize};
use crate::{CalendarPoint, RuntimeState};
fn raw_u32_to_f32_text(raw: u32) -> String {
format!("{:.6}", f32::from_bits(raw))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeSummary {
pub calendar: CalendarPoint,
@ -39,6 +43,10 @@ pub struct RuntimeSummary {
pub metadata_count: usize,
pub company_count: usize,
pub active_company_count: usize,
pub company_market_state_owner_count: usize,
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 player_count: usize,
pub chairman_profile_count: usize,
pub active_chairman_profile_count: usize,
@ -122,6 +130,9 @@ pub struct RuntimeSummary {
impl RuntimeSummary {
pub fn from_state(state: &RuntimeState) -> Self {
let selected_company_market_state = state
.selected_company_id
.and_then(|company_id| state.service_state.company_market_state.get(&company_id));
Self {
calendar: state.calendar,
calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(),
@ -214,6 +225,15 @@ impl RuntimeSummary {
.iter()
.filter(|company| company.active)
.count(),
company_market_state_owner_count: state.service_state.company_market_state.len(),
selected_company_outstanding_shares: selected_company_market_state
.map(|market_state| market_state.outstanding_shares),
selected_company_cached_share_price_value_f32_text: selected_company_market_state
.map(|market_state| raw_u32_to_f32_text(market_state.cached_share_price_raw_u32)),
selected_company_mutable_support_scalar_value_f32_text: selected_company_market_state
.map(|market_state| {
raw_u32_to_f32_text(market_state.mutable_support_scalar_raw_u32)
}),
player_count: state.players.len(),
chairman_profile_count: state.chairman_profiles.len(),
active_chairman_profile_count: state
@ -1765,4 +1785,100 @@ mod tests {
let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.packed_event_blocked_shell_owned_descriptor_count, 1);
}
#[test]
fn summarizes_selected_company_market_state() {
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
active: true,
available_track_laying_capacity: None,
controller_kind: crate::RuntimeCompanyControllerKind::Unknown,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
}],
selected_company_id: Some(1),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
all_cargo_price_override: None,
named_cargo_price_overrides: BTreeMap::new(),
all_cargo_production_override: None,
factory_cargo_production_override: None,
farm_mine_cargo_production_override: None,
named_cargo_production_overrides: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
world_runtime_variables: BTreeMap::new(),
company_runtime_variables: BTreeMap::new(),
player_runtime_variables: BTreeMap::new(),
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState {
company_market_state: BTreeMap::from([(
1,
crate::RuntimeCompanyMarketState {
outstanding_shares: 20_000,
mutable_support_scalar_raw_u32: 0x3f800000,
young_company_support_scalar_raw_u32: 0x42340000,
support_progress_word: 12,
recent_per_share_subscore_raw_u32: 0x420c0000,
cached_share_price_raw_u32: 0x42200000,
chairman_salary_baseline: 24,
chairman_salary_current: 30,
founding_year: 1831,
last_bankruptcy_year: 0,
current_issue_calendar_word: 5,
prior_issue_calendar_word: 4,
city_connection_latch: true,
linked_transit_latch: false,
},
)]),
..RuntimeServiceState::default()
},
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(summary.company_market_state_owner_count, 1);
assert_eq!(summary.selected_company_outstanding_shares, Some(20_000));
assert_eq!(
summary.selected_company_cached_share_price_value_f32_text,
Some("40.000000".to_string())
);
assert_eq!(
summary.selected_company_mutable_support_scalar_value_f32_text,
Some("1.000000".to_string())
);
}
}

View file

@ -109,7 +109,11 @@ The highest-value next passes are now:
candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate
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
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
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
instead of guessing another derived leaf field from neighboring raw offsets

View file

@ -69,7 +69,10 @@ Implemented today:
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;
the company-governance issue lanes; the next shared company-side owning state is rehosted now
too, because save-native company direct records now project into a typed runtime
`company_market_state` cache map carrying outstanding shares, support/share-price/cache words,
chairman salary lanes, calendar words, and connection latches for each live company;
and `runtime inspect-save-company-chairman <save.gms>` 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
@ -189,8 +192,11 @@ frontier is no longer anonymous id recovery; it is the remaining recovered-but-n
families from the checked-in semantic catalog, especially cargo-price, add-building, and other
descriptor clusters that now have explicit shell-owned or evidence-blocked status but not yet a
bounded executable landing surface. Raw save reconstruction for company/chairman context is still a
later tranche once stronger evidence exists. Richer runtime ownership should still be added only
where a later descriptor or condition family needs more than the current event-owned roster.
later tranche once stronger evidence exists, but the current project rule is explicit: prefer
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.
## Why This Boundary