Rehost company bond slots and current stat seam

This commit is contained in:
Jan Petykiewicz 2026-04-17 23:09:06 -07:00
commit 2239f9ce72
5 changed files with 251 additions and 47 deletions

View file

@ -5181,6 +5181,7 @@ mod tests {
market_state: Some(crate::RuntimeCompanyMarketState {
outstanding_shares: 20_000,
bond_count: 2,
live_bond_slots: Vec::new(),
largest_live_bond_principal: Some(500_000),
highest_coupon_live_bond_principal: Some(350_000),
mutable_support_scalar_raw_u32: 0x3f99999a,
@ -5235,6 +5236,7 @@ mod tests {
market_state: Some(crate::RuntimeCompanyMarketState {
outstanding_shares: 18_000,
bond_count: 1,
live_bond_slots: Vec::new(),
largest_live_bond_principal: Some(300_000),
highest_coupon_live_bond_principal: Some(300_000),
mutable_support_scalar_raw_u32: 0x3f4ccccd,
@ -6619,6 +6621,7 @@ mod tests {
crate::RuntimeCompanyMarketState {
outstanding_shares: 30_000,
bond_count: 3,
live_bond_slots: Vec::new(),
largest_live_bond_principal: Some(750_000),
highest_coupon_live_bond_principal: Some(500_000),
mutable_support_scalar_raw_u32: 0x3f19999a,

View file

@ -52,21 +52,22 @@ pub use runtime::{
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget,
RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile,
RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyAnnualFinanceState,
RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric,
RuntimeCompanyMarketState, RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate,
RuntimeCompanyStatSelector, 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,
RuntimeWorldIssueState, RuntimeWorldRestoreState, runtime_company_annual_finance_state,
runtime_company_assigned_share_pool, runtime_company_market_value, runtime_company_prime_rate,
runtime_company_stat_value, runtime_company_stat_value_f64,
RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind,
RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, RuntimeCompanyMetric,
RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, 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, RuntimeWorldIssueState, RuntimeWorldRestoreState,
runtime_company_annual_finance_state, runtime_company_assigned_share_pool,
runtime_company_average_live_bond_coupon, runtime_company_market_value,
runtime_company_prime_rate, runtime_company_stat_value, runtime_company_stat_value_f64,
runtime_company_unassigned_share_pool, runtime_world_issue_opinion_multiplier,
runtime_world_issue_opinion_term_sum_raw, runtime_world_issue_state,
runtime_world_prime_rate_baseline,

View file

@ -106,6 +106,15 @@ pub struct RuntimeCompanyMarketState {
pub special_stat_family_232a_qword_bits: Vec<u64>,
#[serde(default)]
pub issue_opinion_terms_raw_i32: Vec<i32>,
#[serde(default)]
pub live_bond_slots: Vec<RuntimeCompanyBondSlot>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCompanyBondSlot {
pub slot_index: u32,
pub principal: u32,
pub coupon_rate_raw_u32: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -1982,6 +1991,16 @@ pub fn runtime_company_stat_value_f64(
}
}
fn runtime_company_current_stat_value_f64(
state: &RuntimeState,
company_id: u32,
slot_id: u32,
) -> Option<f64> {
let market_state = state.service_state.company_market_state.get(&company_id)?;
let index = slot_id.checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? as usize;
runtime_decode_saved_f64_bits(*market_state.year_stat_family_qword_bits.get(index)?)
}
fn runtime_company_control_transfer_stat_value_f64(
state: &RuntimeState,
company_id: u32,
@ -1992,7 +2011,16 @@ fn runtime_company_control_transfer_stat_value_f64(
.iter()
.find(|company| company.company_id == company_id)?;
match slot_id {
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH => Some(company.current_cash as f64),
0x00..=0x12 if slot_id != RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH => {
runtime_company_current_stat_value_f64(state, company_id, slot_id)
}
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH => {
if company.current_cash != 0 {
Some(company.current_cash as f64)
} else {
runtime_company_current_stat_value_f64(state, company_id, slot_id)
}
}
RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE => Some(company.book_value_per_share as f64),
_ => None,
}
@ -2273,6 +2301,30 @@ pub fn runtime_company_prime_rate(state: &RuntimeState, company_id: u32) -> Opti
runtime_round_f64_to_i64(baseline + (raw_issue_sum as f64) * 0.01)
}
pub fn runtime_company_average_live_bond_coupon(
state: &RuntimeState,
company_id: u32,
) -> Option<f64> {
let market_state = state.service_state.company_market_state.get(&company_id)?;
if market_state.live_bond_slots.is_empty() {
return Some(0.0);
}
let mut weighted_coupon_sum = 0.0f64;
let mut total_principal = 0u64;
for slot in &market_state.live_bond_slots {
let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32) as f64;
if !coupon_rate.is_finite() {
continue;
}
weighted_coupon_sum += coupon_rate * (slot.principal as f64);
total_principal = total_principal.checked_add(slot.principal as u64)?;
}
if total_principal == 0 {
return Some(0.0);
}
Some(weighted_coupon_sum / total_principal as f64)
}
pub fn runtime_world_absolute_counter(state: &RuntimeState) -> Option<u32> {
state.world_restore.absolute_counter_raw_u32
}
@ -4403,6 +4455,13 @@ mod tests {
#[test]
fn reads_grounded_company_stat_family_slots_from_runtime_state() {
let mut year_stat_family_qword_bits = vec![
0u64;
(RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize
];
year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] =
75.0f64.to_bits();
let state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
@ -4460,7 +4519,16 @@ mod tests {
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
service_state: RuntimeServiceState {
company_market_state: BTreeMap::from([(
7,
RuntimeCompanyMarketState {
year_stat_family_qword_bits,
..RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
assert_eq!(
@ -4485,6 +4553,17 @@ mod tests {
),
Some(2_620)
);
assert_eq!(
runtime_company_stat_value(
&state,
7,
RuntimeCompanyStatSelector {
family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
slot_id: 0x12,
},
),
Some(75)
);
assert_eq!(
runtime_company_stat_value(
&state,
@ -4494,7 +4573,7 @@ mod tests {
slot_id: 0x2b,
},
),
None
Some(0)
);
assert_eq!(
runtime_company_stat_value(
@ -5327,6 +5406,93 @@ mod tests {
assert_eq!(runtime_company_prime_rate(&state, 7), Some(7));
}
#[test]
fn computes_weighted_average_live_bond_coupon_from_owned_market_slots() {
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: 7,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
active: true,
available_track_laying_capacity: None,
controller_kind: 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: None,
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([(
7,
RuntimeCompanyMarketState {
live_bond_slots: vec![
RuntimeCompanyBondSlot {
slot_index: 0,
principal: 100_000,
coupon_rate_raw_u32: 0.04f32.to_bits(),
},
RuntimeCompanyBondSlot {
slot_index: 1,
principal: 300_000,
coupon_rate_raw_u32: 0.08f32.to_bits(),
},
],
..RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
let average = runtime_company_average_live_bond_coupon(&state, 7)
.expect("weighted average live bond coupon");
assert!((average - 0.07).abs() < 1e-6);
}
#[test]
fn decodes_and_packs_company_issue_calendar_tuple() {
let tuple = runtime_decode_packed_calendar_tuple(0x0101_0726, 0x0001_0001);

View file

@ -2388,6 +2388,8 @@ pub struct SmpSaveCompanyRecordAnalysisEntry {
pub debt: u64,
pub bond_count: u8,
#[serde(default)]
pub live_bond_slots: Vec<crate::RuntimeCompanyBondSlot>,
#[serde(default)]
pub largest_live_bond_principal: Option<u32>,
#[serde(default)]
pub highest_coupon_live_bond_principal: Option<u32>,
@ -3065,6 +3067,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
&bytes,
record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET,
)?;
let live_bond_slots = parse_save_company_live_bond_slots(&bytes, record_offset)?;
let largest_live_bond_principal =
parse_save_company_largest_live_bond_principal(&bytes, record_offset)?;
let highest_coupon_live_bond_principal =
@ -3168,6 +3171,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
outstanding_shares,
debt,
bond_count,
live_bond_slots,
largest_live_bond_principal,
highest_coupon_live_bond_principal,
available_track_laying_capacity,
@ -3697,6 +3701,7 @@ fn parse_save_company_roster_probe(
)?;
let debt = parse_save_company_total_debt(bytes, record_offset)?;
let bond_count = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)?;
let live_bond_slots = parse_save_company_live_bond_slots(bytes, record_offset)?;
let largest_live_bond_principal =
parse_save_company_largest_live_bond_principal(bytes, record_offset)?;
let highest_coupon_live_bond_principal =
@ -3822,11 +3827,17 @@ fn parse_save_company_roster_probe(
SAVE_COMPANY_RECORD_ISSUE_OPINION_TERMS_OFFSET,
SAVE_COMPANY_RECORD_ISSUE_OPINION_TERM_COUNT,
)?;
let current_cash = decode_save_company_current_year_stat_slot(
&year_stat_family_qword_bits,
crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)
.and_then(round_f64_to_i64)
.unwrap_or(0);
entries.push(SmpLoadedCompanyRosterEntry {
company_id,
active,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 0,
current_cash,
debt,
credit_rating_score: None,
prime_rate: None,
@ -3841,6 +3852,7 @@ fn parse_save_company_roster_probe(
market_state: Some(RuntimeCompanyMarketState {
outstanding_shares,
bond_count,
live_bond_slots,
largest_live_bond_principal,
highest_coupon_live_bond_principal,
mutable_support_scalar_raw_u32,
@ -3947,6 +3959,15 @@ fn build_save_i32_term_strip(
.collect::<Option<Vec<_>>>()
}
fn decode_save_company_current_year_stat_slot(
year_stat_family_qword_bits: &[u64],
slot_id: u32,
) -> Option<f64> {
let index = slot_id.checked_mul(crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? as usize;
let value = f64::from_bits(*year_stat_family_qword_bits.get(index)?);
value.is_finite().then_some(value)
}
fn detect_save_company_record_start_offset(
bytes: &[u8],
header_probe: &SmpSaveTaggedCollectionHeaderProbe,
@ -4054,37 +4075,13 @@ fn parse_save_company_total_debt(bytes: &[u8], record_offset: usize) -> Option<u
Some(total)
}
fn parse_save_company_largest_live_bond_principal(
fn parse_save_company_live_bond_slots(
bytes: &[u8],
record_offset: usize,
) -> Option<Option<u32>> {
) -> Option<Vec<crate::RuntimeCompanyBondSlot>> {
let bond_count =
read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize;
let mut largest_live_principal: Option<u32> = None;
for slot_index in 0..bond_count {
let slot_offset = record_offset
.checked_add(SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET)?
.checked_add(slot_index.checked_mul(SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE)?)?;
let principal = read_i32_at(bytes, slot_offset)?;
if principal > 0 {
let principal = principal as u32;
largest_live_principal = Some(match largest_live_principal {
Some(current) => current.max(principal),
None => principal,
});
}
}
Some(largest_live_principal)
}
fn parse_save_company_highest_coupon_live_bond_principal(
bytes: &[u8],
record_offset: usize,
) -> Option<Option<u32>> {
let bond_count =
read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize;
let mut highest_coupon_principal = None;
let mut highest_coupon_rate = None;
let mut slots = Vec::new();
for slot_index in 0..bond_count {
let slot_offset = record_offset
.checked_add(SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET)?
@ -4098,19 +4095,49 @@ fn parse_save_company_highest_coupon_live_bond_principal(
if !coupon_rate.is_finite() {
continue;
}
let principal = principal as u32;
slots.push(crate::RuntimeCompanyBondSlot {
slot_index: slot_index as u32,
principal: principal as u32,
coupon_rate_raw_u32,
});
}
Some(slots)
}
fn parse_save_company_largest_live_bond_principal(
bytes: &[u8],
record_offset: usize,
) -> Option<Option<u32>> {
let mut largest_live_principal: Option<u32> = None;
for slot in parse_save_company_live_bond_slots(bytes, record_offset)? {
largest_live_principal = Some(match largest_live_principal {
Some(current) => current.max(slot.principal),
None => slot.principal,
});
}
Some(largest_live_principal)
}
fn parse_save_company_highest_coupon_live_bond_principal(
bytes: &[u8],
record_offset: usize,
) -> Option<Option<u32>> {
let mut highest_coupon_principal = None;
let mut highest_coupon_rate = None;
for slot in parse_save_company_live_bond_slots(bytes, record_offset)? {
let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32);
match highest_coupon_rate {
Some(current_rate) if coupon_rate < current_rate => {}
Some(current_rate) if coupon_rate == current_rate => {
if let Some(current_principal) = highest_coupon_principal {
if principal > current_principal {
highest_coupon_principal = Some(principal);
if slot.principal > current_principal {
highest_coupon_principal = Some(slot.principal);
}
}
}
_ => {
highest_coupon_rate = Some(coupon_rate);
highest_coupon_principal = Some(principal);
highest_coupon_principal = Some(slot.principal);
}
}
}
@ -16466,6 +16493,12 @@ mod tests {
.as_ref()
.expect("company market state should load");
assert_eq!(market_state.outstanding_shares, 20_000);
assert_eq!(market_state.live_bond_slots.len(), 2);
assert_eq!(market_state.live_bond_slots[0].principal, 900_000);
assert_eq!(
market_state.live_bond_slots[1].coupon_rate_raw_u32,
0.12f32.to_bits()
);
assert_eq!(market_state.largest_live_bond_principal, Some(900_000));
assert_eq!(
market_state.highest_coupon_live_bond_principal,

View file

@ -2039,6 +2039,7 @@ mod tests {
crate::RuntimeCompanyMarketState {
outstanding_shares: 20_000,
bond_count: 2,
live_bond_slots: Vec::new(),
highest_coupon_live_bond_principal: Some(350_000),
largest_live_bond_principal: Some(500_000),
mutable_support_scalar_raw_u32: 0x3f800000,