Rehost direct company control-transfer field band

This commit is contained in:
Jan Petykiewicz 2026-04-17 23:33:17 -07:00
commit 87e8742f37
6 changed files with 343 additions and 8 deletions

View file

@ -5208,6 +5208,8 @@ mod tests {
year_stat_family_qword_bits: Vec::new(),
special_stat_family_232a_qword_bits: Vec::new(),
issue_opinion_terms_raw_i32: Vec::new(),
direct_control_transfer_float_fields_raw_u32: BTreeMap::new(),
direct_control_transfer_int_fields_raw_u32: BTreeMap::new(),
}),
},
crate::SmpLoadedCompanyRosterEntry {
@ -5263,6 +5265,8 @@ mod tests {
year_stat_family_qword_bits: Vec::new(),
special_stat_family_232a_qword_bits: Vec::new(),
issue_opinion_terms_raw_i32: Vec::new(),
direct_control_transfer_float_fields_raw_u32: BTreeMap::new(),
direct_control_transfer_int_fields_raw_u32: BTreeMap::new(),
}),
},
],
@ -6648,6 +6652,8 @@ mod tests {
year_stat_family_qword_bits: Vec::new(),
special_stat_family_232a_qword_bits: Vec::new(),
issue_opinion_terms_raw_i32: Vec::new(),
direct_control_transfer_float_fields_raw_u32: BTreeMap::new(),
direct_control_transfer_int_fields_raw_u32: BTreeMap::new(),
},
)]),
world_issue_opinion_base_terms_raw_i32: Vec::new(),

View file

@ -66,11 +66,12 @@ pub use runtime::{
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_credit_rating,
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,
runtime_company_average_live_bond_coupon, runtime_company_book_value_per_share,
runtime_company_credit_rating, 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,
};
pub use smp::{
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,

View file

@ -108,6 +108,10 @@ pub struct RuntimeCompanyMarketState {
pub issue_opinion_terms_raw_i32: Vec<i32>,
#[serde(default)]
pub live_bond_slots: Vec<RuntimeCompanyBondSlot>,
#[serde(default)]
pub direct_control_transfer_float_fields_raw_u32: BTreeMap<u32, u32>,
#[serde(default)]
pub direct_control_transfer_int_fields_raw_u32: BTreeMap<u32, u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -2002,6 +2006,47 @@ fn runtime_company_current_stat_value_f64(
runtime_decode_saved_f64_bits(*market_state.year_stat_family_qword_bits.get(index)?)
}
fn runtime_company_direct_float_field_value_f64(
state: &RuntimeState,
company_id: u32,
field_offset: u32,
) -> Option<f64> {
let market_state = state.service_state.company_market_state.get(&company_id)?;
let raw_u32 = *market_state
.direct_control_transfer_float_fields_raw_u32
.get(&field_offset)?;
let value = f32::from_bits(raw_u32) as f64;
if !value.is_finite() {
return None;
}
Some(value)
}
fn runtime_company_direct_i32_field_value_f64(
state: &RuntimeState,
company_id: u32,
field_offset: u32,
) -> Option<f64> {
let market_state = state.service_state.company_market_state.get(&company_id)?;
Some(i64::from(
*market_state
.direct_control_transfer_int_fields_raw_u32
.get(&field_offset)? as i32,
) as f64)
}
pub fn runtime_company_book_value_per_share(state: &RuntimeState, company_id: u32) -> Option<i64> {
let company = state
.companies
.iter()
.find(|company| company.company_id == company_id)?;
if company.book_value_per_share != 0 {
return Some(company.book_value_per_share);
}
runtime_company_direct_float_field_value_f64(state, company_id, 0x32f)
.and_then(runtime_round_f64_to_i64)
}
fn runtime_company_control_transfer_stat_value_f64(
state: &RuntimeState,
company_id: u32,
@ -2022,10 +2067,98 @@ fn runtime_company_control_transfer_stat_value_f64(
runtime_company_current_stat_value_f64(state, company_id, slot_id)
}
}
0x14 => runtime_company_control_transfer_stat_value_f64(state, company_id, 0x31)
.zip(state.service_state.company_market_state.get(&company_id))
.map(|(value, market_state)| {
if market_state.outstanding_shares == 0 {
0.0
} else {
value / market_state.outstanding_shares as f64
}
}),
0x15 => runtime_company_stat_value_f64(
state,
company_id,
RuntimeCompanyStatSelector {
family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
slot_id: 0x2c,
},
)
.zip(state.service_state.company_market_state.get(&company_id))
.map(|(value, market_state)| {
if market_state.outstanding_shares == 0 {
0.0
} else {
value / market_state.outstanding_shares as f64
}
}),
0x16 => runtime_company_stat_value_f64(
state,
company_id,
RuntimeCompanyStatSelector {
family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
slot_id: 0x2b,
},
)
.zip(state.service_state.company_market_state.get(&company_id))
.map(|(value, market_state)| {
if market_state.outstanding_shares == 0 {
0.0
} else {
value / market_state.outstanding_shares as f64
}
}),
0x17 => runtime_company_direct_float_field_value_f64(state, company_id, 0x4b),
RUNTIME_COMPANY_STAT_SLOT_CREDIT_RATING => {
runtime_company_credit_rating(state, company_id).map(|value| value as f64)
}
RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE => Some(company.book_value_per_share as f64),
0x1a => runtime_company_direct_float_field_value_f64(state, company_id, 0x53),
0x1b => runtime_company_direct_float_field_value_f64(state, company_id, 0x323),
0x1c => runtime_company_direct_float_field_value_f64(state, company_id, 0x327),
RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE => {
runtime_company_book_value_per_share(state, company_id).map(|value| value as f64)
}
0x1e => runtime_company_direct_float_field_value_f64(state, company_id, 0x333),
0x1f => runtime_company_direct_float_field_value_f64(state, company_id, 0x33b),
0x20 => runtime_company_direct_float_field_value_f64(state, company_id, 0x33f),
0x21 => runtime_company_direct_float_field_value_f64(state, company_id, 0x327).and_then(
|denominator| {
let numerator =
runtime_company_direct_float_field_value_f64(state, company_id, 0x32b)?;
Some(if denominator <= 0.0 {
numerator
} else {
numerator / denominator
})
},
),
0x22 => runtime_company_direct_float_field_value_f64(state, company_id, 0x333).and_then(
|denominator| {
let numerator =
runtime_company_direct_float_field_value_f64(state, company_id, 0x337)?;
Some(if denominator <= 0.0 {
numerator
} else {
numerator / denominator
})
},
),
0x23 => runtime_company_direct_float_field_value_f64(state, company_id, 0x33f).and_then(
|denominator| {
let numerator =
runtime_company_direct_float_field_value_f64(state, company_id, 0x343)?;
Some(if denominator <= 0.0 {
numerator
} else {
numerator / denominator
})
},
),
0x26 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x34b),
0x27 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x14f),
0x28 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d0b),
0x29 => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d0f),
0x2a => runtime_company_direct_i32_field_value_f64(state, company_id, 0x0d13),
_ => None,
}
}
@ -4733,6 +4866,94 @@ mod tests {
);
}
#[test]
fn reads_book_value_per_share_from_rehosted_direct_company_field_band() {
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 {
direct_control_transfer_float_fields_raw_u32: BTreeMap::from([(
0x32f,
2620.0f32.to_bits(),
)]),
..RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
assert_eq!(runtime_company_book_value_per_share(&state, 7), Some(2620));
assert_eq!(
runtime_company_stat_value(
&state,
7,
RuntimeCompanyStatSelector {
family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
slot_id: RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE,
},
),
Some(2620)
);
}
#[test]
fn reads_year_relative_company_stat_family_from_saved_market_matrix() {
let mut year_stat_family_qword_bits = vec![

View file

@ -3609,6 +3609,11 @@ const SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT: 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_DIRECT_CONTROL_TRANSFER_FLOAT_FIELDS: [usize; 10] = [
0x4b, 0x53, 0x323, 0x327, 0x32b, 0x32f, 0x333, 0x337, 0x33b, 0x33f,
];
const SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_INT_FIELDS: [usize; 5] =
[0x14f, 0x34b, 0x0d0b, 0x0d0f, 0x0d13];
const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 9] = [
("mutable_support_scalar", 0x4f),
("young_company_support_scalar", 0x57),
@ -3821,6 +3826,16 @@ fn parse_save_company_roster_probe(
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET,
SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT,
)?;
let direct_control_transfer_float_fields_raw_u32 = build_save_u32_field_map(
bytes,
record_offset,
&SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_FLOAT_FIELDS,
)?;
let direct_control_transfer_int_fields_raw_u32 = build_save_u32_field_map(
bytes,
record_offset,
&SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_INT_FIELDS,
)?;
let issue_opinion_terms_raw_i32 = build_save_i32_term_strip(
bytes,
record_offset,
@ -3888,6 +3903,8 @@ fn parse_save_company_roster_probe(
year_stat_family_qword_bits,
special_stat_family_232a_qword_bits,
issue_opinion_terms_raw_i32,
direct_control_transfer_float_fields_raw_u32,
direct_control_transfer_int_fields_raw_u32,
}),
});
}
@ -3959,6 +3976,21 @@ fn build_save_i32_term_strip(
.collect::<Option<Vec<_>>>()
}
fn build_save_u32_field_map(
bytes: &[u8],
record_offset: usize,
offsets: &[usize],
) -> Option<BTreeMap<u32, u32>> {
let mut fields = BTreeMap::new();
for relative_offset in offsets {
fields.insert(
u32::try_from(*relative_offset).ok()?,
read_u32_at(bytes, record_offset + *relative_offset)?,
);
}
Some(fields)
}
fn decode_save_company_current_year_stat_slot(
year_stat_family_qword_bits: &[u64],
slot_id: u32,

View file

@ -8,7 +8,8 @@ use crate::{
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator,
RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerTarget, RuntimeState, RuntimeSummary,
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
calendar::BoundaryEventKind, runtime_company_credit_rating, runtime_company_prime_rate,
calendar::BoundaryEventKind, runtime_company_book_value_per_share,
runtime_company_credit_rating, runtime_company_prime_rate,
};
const PERIODIC_TRIGGER_KIND_ORDER: [u8; 6] = [1, 0, 3, 2, 5, 4];
@ -1522,7 +1523,9 @@ fn company_metric_value(
RuntimeCompanyMetric::PrimeRate => {
runtime_company_prime_rate(state, company.company_id).unwrap_or(0)
}
RuntimeCompanyMetric::BookValuePerShare => company.book_value_per_share,
RuntimeCompanyMetric::BookValuePerShare => {
runtime_company_book_value_per_share(state, company.company_id).unwrap_or(0)
}
RuntimeCompanyMetric::InvestorConfidence => company.investor_confidence,
RuntimeCompanyMetric::ManagementAttitude => company.management_attitude,
RuntimeCompanyMetric::TrackPiecesTotal => i64::from(company.track_piece_counts.total),
@ -4144,6 +4147,76 @@ mod tests {
);
}
#[test]
fn book_value_condition_reads_rehosted_direct_company_field_band() {
let mut state = RuntimeState {
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 37,
management_attitude: 58,
takeover_cooldown_year: Some(1844),
merger_cooldown_year: Some(1845),
}],
selected_company_id: Some(1),
service_state: RuntimeServiceState {
company_market_state: BTreeMap::from([(
1,
crate::RuntimeCompanyMarketState {
direct_control_transfer_float_fields_raw_u32: BTreeMap::from([(
0x32f,
2620.0f32.to_bits(),
)]),
..crate::RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
event_runtime_records: vec![RuntimeEventRecord {
record_id: 197,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: vec![RuntimeCondition::CompanyNumericThreshold {
target: RuntimeCompanyTarget::SelectedCompany,
metric: crate::RuntimeCompanyMetric::BookValuePerShare,
comparator: RuntimeConditionComparator::Eq,
value: 2620,
}],
effects: vec![RuntimeEffect::SetWorldFlag {
key: "world.rehosted_book_value_gate_passed".to_string(),
value: true,
}],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("rehosted direct-field book-value condition should gate execution");
assert_eq!(
state
.world_flags
.get("world.rehosted_book_value_gate_passed"),
Some(&true)
);
}
#[test]
fn derived_credit_rating_condition_reads_rehosted_finance_owner_state() {
let mut year_stat_family_qword_bits = vec![

View file

@ -2105,6 +2105,8 @@ mod tests {
year_stat_family_qword_bits: Vec::new(),
special_stat_family_232a_qword_bits: Vec::new(),
issue_opinion_terms_raw_i32: Vec::new(),
direct_control_transfer_float_fields_raw_u32: BTreeMap::new(),
direct_control_transfer_int_fields_raw_u32: BTreeMap::new(),
},
)]),
..RuntimeServiceState::default()