Rehost saved company year stat families

This commit is contained in:
Jan Petykiewicz 2026-04-17 22:34:58 -07:00
commit cbf0dbeda5
5 changed files with 366 additions and 13 deletions

View file

@ -5148,6 +5148,8 @@ mod tests {
stat_band_root_0cfb_candidates: Vec::new(),
stat_band_root_0d7f_candidates: Vec::new(),
stat_band_root_1c47_candidates: Vec::new(),
year_stat_family_qword_bits: Vec::new(),
special_stat_family_232a_qword_bits: Vec::new(),
}),
},
crate::SmpLoadedCompanyRosterEntry {
@ -5199,6 +5201,8 @@ mod tests {
stat_band_root_0cfb_candidates: Vec::new(),
stat_band_root_0d7f_candidates: Vec::new(),
stat_band_root_1c47_candidates: Vec::new(),
year_stat_family_qword_bits: Vec::new(),
special_stat_family_232a_qword_bits: Vec::new(),
}),
},
],
@ -6583,6 +6587,8 @@ mod tests {
stat_band_root_0cfb_candidates: Vec::new(),
stat_band_root_0d7f_candidates: Vec::new(),
stat_band_root_1c47_candidates: Vec::new(),
year_stat_family_qword_bits: Vec::new(),
special_stat_family_232a_qword_bits: Vec::new(),
},
)]),
..RuntimeServiceState::default()

View file

@ -44,10 +44,11 @@ pub use pk4::{
extract_pk4_entry_bytes, extract_pk4_entry_file, inspect_pk4_bytes, inspect_pk4_file,
};
pub use runtime::{
RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_WORLD_ISSUE_CREDIT_MARKET,
RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE, RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE,
RUNTIME_WORLD_ISSUE_PRIME_RATE,
RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A,
RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, RUNTIME_COMPANY_STAT_SLOT_COUNT,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN,
RUNTIME_WORLD_ISSUE_CREDIT_MARKET, RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE,
RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE, RUNTIME_WORLD_ISSUE_PRIME_RATE,
RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget,
RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile,
RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyAnnualFinanceState,
@ -65,7 +66,8 @@ pub use runtime::{
RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate,
RuntimeWorldIssueState, RuntimeWorldRestoreState, runtime_company_annual_finance_state,
runtime_company_assigned_share_pool, runtime_company_market_value, runtime_company_stat_value,
runtime_company_unassigned_share_pool, runtime_world_issue_state,
runtime_company_stat_value_f64, runtime_company_unassigned_share_pool,
runtime_world_issue_state,
};
pub use smp::{
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,

View file

@ -100,6 +100,10 @@ pub struct RuntimeCompanyMarketState {
pub stat_band_root_0d7f_candidates: Vec<RuntimeCompanyStatBandCandidate>,
#[serde(default)]
pub stat_band_root_1c47_candidates: Vec<RuntimeCompanyStatBandCandidate>,
#[serde(default)]
pub year_stat_family_qword_bits: Vec<u64>,
#[serde(default)]
pub special_stat_family_232a_qword_bits: Vec<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -413,8 +417,11 @@ pub struct RuntimeCompanyStatSelector {
}
pub const RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER: u32 = 0x2329;
pub const RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A: u32 = 0x232a;
pub const RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH: u32 = 0x0d;
pub const RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE: u32 = 0x1d;
pub const RUNTIME_COMPANY_STAT_SLOT_COUNT: u32 = 0x2b;
pub const RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN: u32 = 11;
pub const RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE: u32 = 0x37;
pub const RUNTIME_WORLD_ISSUE_CREDIT_MARKET: u32 = 0x38;
pub const RUNTIME_WORLD_ISSUE_PRIME_RATE: u32 = 0x39;
@ -1920,22 +1927,133 @@ pub fn runtime_company_stat_value(
company_id: u32,
selector: RuntimeCompanyStatSelector,
) -> Option<i64> {
runtime_company_stat_value_f64(state, company_id, selector).and_then(runtime_round_f64_to_i64)
}
pub fn runtime_company_stat_value_f64(
state: &RuntimeState,
company_id: u32,
selector: RuntimeCompanyStatSelector,
) -> Option<f64> {
if selector.slot_id >= RUNTIME_COMPANY_STAT_SLOT_COUNT {
return runtime_company_derived_stat_value_f64(
state,
company_id,
selector.family_id,
selector.slot_id,
);
}
match selector.family_id {
RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER => {
runtime_company_control_transfer_stat_value_f64(state, company_id, selector.slot_id)
}
RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A => {
runtime_company_special_stat_family_232a_value_f64(state, company_id, selector.slot_id)
}
family_id => runtime_company_year_stat_value_f64(state, company_id, family_id, selector.slot_id),
}
}
fn runtime_company_control_transfer_stat_value_f64(
state: &RuntimeState,
company_id: u32,
slot_id: u32,
) -> Option<f64> {
let company = state
.companies
.iter()
.find(|company| company.company_id == company_id)?;
match (selector.family_id, selector.slot_id) {
(RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH) => {
Some(company.current_cash)
}
(
RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE,
) => Some(company.book_value_per_share),
match slot_id {
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH => Some(company.current_cash as f64),
RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE => Some(company.book_value_per_share as f64),
_ => None,
}
}
fn runtime_company_special_stat_family_232a_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 value = runtime_decode_saved_f64_bits(
*market_state
.special_stat_family_232a_qword_bits
.get(slot_id as usize)?,
)?;
if (0x13..=0x1b).contains(&slot_id) {
Some(value + runtime_company_control_transfer_stat_value_f64(state, company_id, slot_id)?)
} else {
Some(value)
}
}
fn runtime_company_year_stat_value_f64(
state: &RuntimeState,
company_id: u32,
family_id: u32,
slot_id: u32,
) -> Option<f64> {
let current_year_word = u32::from(state.world_restore.packed_year_word_raw_u16?);
let year_delta = current_year_word.checked_sub(family_id)?;
if year_delta == 0 || year_delta >= RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN {
return None;
}
let market_state = state.service_state.company_market_state.get(&company_id)?;
let index = slot_id
.checked_mul(RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)?
.checked_add(year_delta)? as usize;
runtime_decode_saved_f64_bits(*market_state.year_stat_family_qword_bits.get(index)?)
}
fn runtime_company_derived_stat_value_f64(
state: &RuntimeState,
company_id: u32,
family_id: u32,
slot_id: u32,
) -> Option<f64> {
let stat = |slot_id| {
runtime_company_stat_value_f64(
state,
company_id,
RuntimeCompanyStatSelector { family_id, slot_id },
)
};
let rounded_stat = |slot_id| stat(slot_id).and_then(runtime_round_f64_to_i64);
match slot_id {
0x2b => Some(stat(0x2d)? + stat(0x2c)?),
0x2c => Some(stat(0x04)? + stat(0x03)? + stat(0x02)? + stat(0x01)?),
0x2d => Some(stat(0x2f)? + stat(0x2e)?),
0x2e => Some(
stat(0x0c)?
+ stat(0x0b)?
+ stat(0x0a)?
+ stat(0x08)?
+ stat(0x07)?
+ stat(0x06)?
+ stat(0x05)?,
),
0x2f => stat(0x09),
0x30 => Some(stat(0x11)? + stat(0x10)? + stat(0x0f)? + stat(0x0e)? + stat(0x0d)?),
0x31 => Some(stat(0x30)? + stat(0x12)?),
0x32 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x24)?),
0x33 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x16)?),
0x34 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x17)?),
0x35 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x18)?),
0x36 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x19)?),
0x37 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x1a)?),
0x38 => runtime_divide_by_rounded_stat_i64(stat(0x2c)?, rounded_stat(0x1b)?),
_ => None,
}
}
fn runtime_divide_by_rounded_stat_i64(numerator: f64, denominator: i64) -> Option<f64> {
if denominator == 0 {
return Some(0.0);
}
Some(numerator / denominator as f64)
}
pub fn runtime_world_issue_state(
state: &RuntimeState,
issue_id: u32,
@ -2178,6 +2296,24 @@ fn rounded_cached_share_price_i64(raw_u32: u32) -> Option<i64> {
Some(value.round() as i64)
}
fn runtime_decode_saved_f64_bits(bits: u64) -> Option<f64> {
let value = f64::from_bits(bits);
if !value.is_finite() {
return None;
}
Some(value)
}
fn runtime_round_f64_to_i64(value: f64) -> Option<i64> {
if !value.is_finite() {
return None;
}
if value < i64::MIN as f64 || value > i64::MAX as f64 {
return None;
}
Some(value.round() as i64)
}
fn derive_runtime_company_elapsed_years(current_year: u32, prior_year: u32) -> Option<u32> {
if prior_year == 0 || prior_year > current_year {
return None;
@ -4175,6 +4311,180 @@ mod tests {
);
}
#[test]
fn reads_year_relative_company_stat_family_from_saved_market_matrix() {
let mut year_stat_family_qword_bits =
vec![0u64; (RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize];
let write_year_value = |bits: &mut Vec<u64>, slot_id: u32, year_delta: u32, value: f64| {
let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize;
bits[index] = value.to_bits();
};
write_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10.0);
write_year_value(&mut year_stat_family_qword_bits, 0x02, 1, 20.0);
write_year_value(&mut year_stat_family_qword_bits, 0x03, 1, 30.0);
write_year_value(&mut year_stat_family_qword_bits, 0x04, 1, 40.0);
write_year_value(&mut year_stat_family_qword_bits, 0x05, 1, 5.0);
write_year_value(&mut year_stat_family_qword_bits, 0x06, 1, 6.0);
write_year_value(&mut year_stat_family_qword_bits, 0x07, 1, 7.0);
write_year_value(&mut year_stat_family_qword_bits, 0x08, 1, 8.0);
write_year_value(&mut year_stat_family_qword_bits, 0x09, 1, 9.0);
write_year_value(&mut year_stat_family_qword_bits, 0x0a, 1, 10.0);
write_year_value(&mut year_stat_family_qword_bits, 0x0b, 1, 11.0);
write_year_value(&mut year_stat_family_qword_bits, 0x0c, 1, 12.0);
write_year_value(&mut year_stat_family_qword_bits, 0x0d, 1, 13.0);
write_year_value(&mut year_stat_family_qword_bits, 0x0e, 1, 14.0);
write_year_value(&mut year_stat_family_qword_bits, 0x0f, 1, 15.0);
write_year_value(&mut year_stat_family_qword_bits, 0x10, 1, 16.0);
write_year_value(&mut year_stat_family_qword_bits, 0x11, 1, 17.0);
write_year_value(&mut year_stat_family_qword_bits, 0x12, 1, 18.0);
write_year_value(&mut year_stat_family_qword_bits, 0x16, 1, 4.0);
write_year_value(&mut year_stat_family_qword_bits, 0x17, 1, 10.0);
write_year_value(&mut year_stat_family_qword_bits, 0x18, 1, 20.0);
write_year_value(&mut year_stat_family_qword_bits, 0x19, 1, 25.0);
write_year_value(&mut year_stat_family_qword_bits, 0x1a, 1, 50.0);
write_year_value(&mut year_stat_family_qword_bits, 0x1b, 1, 100.0);
write_year_value(&mut year_stat_family_qword_bits, 0x24, 1, 5.0);
let mut special_stat_family_232a_qword_bits =
vec![0u64; RUNTIME_COMPANY_STAT_SLOT_COUNT as usize];
special_stat_family_232a_qword_bits[RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize] =
111.0f64.to_bits();
let state = RuntimeState {
calendar: CalendarPoint {
year: 1845,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState {
packed_year_word_raw_u16: Some(1845),
..RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 7,
current_cash: 125_000,
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: 2_620,
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 {
year_stat_family_qword_bits,
special_stat_family_232a_qword_bits,
..RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
let prior_year = RuntimeCompanyStatSelector {
family_id: 1844,
slot_id: 0x09,
};
assert_eq!(runtime_company_stat_value_f64(&state, 7, prior_year), Some(9.0));
assert_eq!(
runtime_company_stat_value_f64(
&state,
7,
RuntimeCompanyStatSelector {
family_id: 1844,
slot_id: 0x2c,
},
),
Some(100.0)
);
assert_eq!(
runtime_company_stat_value_f64(
&state,
7,
RuntimeCompanyStatSelector {
family_id: 1844,
slot_id: 0x2b,
},
),
Some(168.0)
);
assert_eq!(
runtime_company_stat_value_f64(
&state,
7,
RuntimeCompanyStatSelector {
family_id: 1844,
slot_id: 0x32,
},
),
Some(20.0)
);
assert_eq!(
runtime_company_stat_value_f64(
&state,
7,
RuntimeCompanyStatSelector {
family_id: 1844,
slot_id: 0x38,
},
),
Some(1.0)
);
assert_eq!(
runtime_company_stat_value_f64(
&state,
7,
RuntimeCompanyStatSelector {
family_id: RUNTIME_COMPANY_STAT_FAMILY_SPECIAL_232A,
slot_id: RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
},
),
Some(111.0)
);
}
#[test]
fn reads_grounded_world_issue_state_from_runtime_restore_state() {
let state = RuntimeState {

View file

@ -3588,6 +3588,11 @@ const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET: usize = 0x0cfb;
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET: usize = 0x0d7f;
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET: usize = 0x1c47;
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS: usize = 32;
const SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT: usize =
crate::runtime::RUNTIME_COMPANY_STAT_SLOT_COUNT as usize
* crate::runtime::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN as usize;
const SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT: usize =
crate::runtime::RUNTIME_COMPANY_STAT_SLOT_COUNT as usize;
const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 9] = [
("mutable_support_scalar", 0x4f),
("young_company_support_scalar", 0x57),
@ -3784,6 +3789,18 @@ fn parse_save_company_roster_probe(
"stat_band_1c47",
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
)?;
let year_stat_family_qword_bits = build_save_company_stat_qword_bits(
bytes,
record_offset,
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET,
SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT,
)?;
let special_stat_family_232a_qword_bits = build_save_company_stat_qword_bits(
bytes,
record_offset,
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET,
SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT,
)?;
entries.push(SmpLoadedCompanyRosterEntry {
company_id,
active,
@ -3835,6 +3852,8 @@ fn parse_save_company_roster_probe(
.iter()
.map(runtime_company_stat_band_candidate_from_save)
.collect(),
year_stat_family_qword_bits,
special_stat_family_232a_qword_bits,
}),
});
}
@ -3878,6 +3897,20 @@ fn build_save_company_stat_band_candidates(
.collect::<Option<Vec<_>>>()
}
fn build_save_company_stat_qword_bits(
bytes: &[u8],
record_offset: usize,
root_offset: usize,
qword_count: usize,
) -> Option<Vec<u64>> {
(0..qword_count)
.map(|index| {
let relative_offset = root_offset.checked_add(index.checked_mul(8)?)?;
read_u64_at(bytes, record_offset + relative_offset)
})
.collect::<Option<Vec<_>>>()
}
fn detect_save_company_record_start_offset(
bytes: &[u8],
header_probe: &SmpSaveTaggedCollectionHeaderProbe,

View file

@ -2102,6 +2102,8 @@ mod tests {
value_f32_text: "0.000000".to_string(),
},
],
year_stat_family_qword_bits: Vec::new(),
special_stat_family_232a_qword_bits: Vec::new(),
},
)]),
..RuntimeServiceState::default()