Rehost annual creditor pressure policy branch

This commit is contained in:
Jan Petykiewicz 2026-04-18 00:09:09 -07:00
commit 64fc4a8488
8 changed files with 739 additions and 17 deletions

View file

@ -89,7 +89,10 @@ bond table now also contributes both the largest live bond principal and the cho
highest-coupon live bond principal into owned company market and annual-finance state, so the
stock-capital approval ladder can extend one rehosted owner-state surface instead of hunting
another isolated finance leaf. A checked-in
The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we
fixed-world finance-policy seam now also carries the raw stock, bond, bankruptcy, and dividend
policy bytes from the `0x32c8` save block, and the first annual creditor-pressure branch now runs
headlessly as a pure runtime reader over owned annual-finance state, support-adjusted share price,
and current world finance policy rather than as a notes-only atlas fragment. 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
`EventEffects` export now exists too in

View file

@ -851,6 +851,38 @@ fn project_save_slice_components(
.world_issue_37_state
.as_ref()
.map(|state| state.multiplier_value_f32_text.clone()),
stock_issue_and_buyback_policy_raw_u8: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.stock_policy_raw_u8),
bond_issue_and_repayment_policy_raw_u8: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.bond_policy_raw_u8),
bankruptcy_policy_raw_u8: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.bankruptcy_policy_raw_u8),
dividend_policy_raw_u8: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.dividend_policy_raw_u8),
stock_issue_and_buyback_allowed: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.stock_policy_raw_u8 == 0),
bond_issue_and_repayment_allowed: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.bond_policy_raw_u8 == 0),
bankruptcy_allowed: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.bankruptcy_policy_raw_u8 == 0),
dividend_adjustment_allowed: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.dividend_policy_raw_u8 == 0),
finance_neighborhood_candidates: save_slice
.world_finance_neighborhood_state
.as_ref()
@ -5979,6 +6011,14 @@ mod tests {
absolute_counter_raw_hex: "0x00000003".to_string(),
absolute_counter_mirror_raw_u32: 4,
absolute_counter_mirror_raw_hex: "0x00000004".to_string(),
stock_policy_raw_u8: 0,
stock_policy_raw_hex: "0x00".to_string(),
bond_policy_raw_u8: 1,
bond_policy_raw_hex: "0x01".to_string(),
bankruptcy_policy_raw_u8: 0,
bankruptcy_policy_raw_hex: "0x00".to_string(),
dividend_policy_raw_u8: 1,
dividend_policy_raw_hex: "0x01".to_string(),
labels: vec![
"current_calendar_tuple_word".to_string(),
"current_calendar_tuple_word_2".to_string(),
@ -6263,6 +6303,35 @@ mod tests {
import.state.world_restore.issue_37_multiplier_raw_u32,
Some(0x3d75c28f)
);
assert_eq!(
import
.state
.world_restore
.stock_issue_and_buyback_policy_raw_u8,
Some(0)
);
assert_eq!(
import
.state
.world_restore
.bond_issue_and_repayment_policy_raw_u8,
Some(1)
);
assert_eq!(import.state.world_restore.bankruptcy_policy_raw_u8, Some(0));
assert_eq!(import.state.world_restore.dividend_policy_raw_u8, Some(1));
assert_eq!(
import.state.world_restore.stock_issue_and_buyback_allowed,
Some(true)
);
assert_eq!(
import.state.world_restore.bond_issue_and_repayment_allowed,
Some(false)
);
assert_eq!(import.state.world_restore.bankruptcy_allowed, Some(true));
assert_eq!(
import.state.world_restore.dividend_adjustment_allowed,
Some(false)
);
assert_eq!(
import
.state

View file

@ -52,12 +52,13 @@ pub use runtime::{
RUNTIME_WORLD_ISSUE_PRIME_RATE, RuntimeCargoCatalogEntry, RuntimeCargoClass,
RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric,
RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany,
RuntimeCompanyAnnualFinanceState, RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope,
RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric, RuntimeCompanyMarketState,
RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector,
RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord,
RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualFinanceState,
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,
@ -65,14 +66,18 @@ pub use runtime::{
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_book_value_per_share,
runtime_company_credit_rating, runtime_company_investor_confidence,
runtime_company_management_attitude, runtime_company_market_value, runtime_company_prime_rate,
runtime_company_annual_creditor_pressure_state, runtime_company_annual_finance_state,
runtime_company_assigned_share_pool, runtime_company_average_live_bond_coupon,
runtime_company_book_value_per_share, runtime_company_credit_rating,
runtime_company_investor_confidence, runtime_company_management_attitude,
runtime_company_market_value, runtime_company_prime_rate,
runtime_company_recent_per_share_subscore, runtime_company_stat_value,
runtime_company_stat_value_f64, runtime_company_unassigned_share_pool,
runtime_world_annual_finance_mode_active, runtime_world_bankruptcy_allowed,
runtime_world_bond_issue_and_repayment_allowed, runtime_world_dividend_adjustment_allowed,
runtime_world_issue_opinion_multiplier, runtime_world_issue_opinion_term_sum_raw,
runtime_world_issue_state, runtime_world_prime_rate_baseline,
runtime_world_stock_issue_and_buyback_allowed,
};
pub use smp::{
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,

View file

@ -177,6 +177,37 @@ pub struct RuntimeCompanyAnnualFinanceState {
pub linked_transit_latch: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCompanyAnnualCreditorPressureState {
pub company_id: u32,
#[serde(default)]
pub annual_mode_active: Option<bool>,
#[serde(default)]
pub bankruptcy_allowed: Option<bool>,
#[serde(default)]
pub years_since_last_bankruptcy: Option<u32>,
#[serde(default)]
pub years_since_founding: Option<u32>,
pub recent_bad_net_profit_year_count: u32,
#[serde(default)]
pub recent_peak_revenue: Option<i64>,
#[serde(default)]
pub recent_three_year_net_profit_total: Option<i64>,
#[serde(default)]
pub pressure_ladder_cash_floor: Option<i64>,
#[serde(default)]
pub current_cash_plus_slot_12_total: Option<i64>,
#[serde(default)]
pub support_adjusted_share_price_floor: Option<i64>,
#[serde(default)]
pub support_adjusted_share_price_scalar: Option<i64>,
#[serde(default)]
pub current_fuel_cost: Option<i64>,
#[serde(default)]
pub current_fuel_cost_floor: Option<i64>,
pub eligible_for_bankruptcy_branch: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RuntimeTrackPieceCounts {
#[serde(default)]
@ -1101,6 +1132,22 @@ pub struct RuntimeWorldRestoreState {
#[serde(default)]
pub issue_37_multiplier_value_f32_text: Option<String>,
#[serde(default)]
pub stock_issue_and_buyback_policy_raw_u8: Option<u8>,
#[serde(default)]
pub bond_issue_and_repayment_policy_raw_u8: Option<u8>,
#[serde(default)]
pub bankruptcy_policy_raw_u8: Option<u8>,
#[serde(default)]
pub dividend_policy_raw_u8: Option<u8>,
#[serde(default)]
pub stock_issue_and_buyback_allowed: Option<bool>,
#[serde(default)]
pub bond_issue_and_repayment_allowed: Option<bool>,
#[serde(default)]
pub bankruptcy_allowed: Option<bool>,
#[serde(default)]
pub dividend_adjustment_allowed: Option<bool>,
#[serde(default)]
pub finance_neighborhood_candidates: Vec<RuntimeWorldFinanceNeighborhoodCandidate>,
#[serde(default)]
pub economic_tuning_mirror_raw_u32: Option<u32>,
@ -2718,6 +2765,135 @@ pub fn runtime_company_average_live_bond_coupon(
Some(weighted_coupon_sum / total_principal as f64)
}
pub fn runtime_world_annual_finance_mode_active(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.partial_year_progress_raw_u8? == 0x0c)
}
pub fn runtime_world_bankruptcy_allowed(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.bankruptcy_policy_raw_u8? == 0)
}
pub fn runtime_world_bond_issue_and_repayment_allowed(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.bond_issue_and_repayment_policy_raw_u8? == 0)
}
pub fn runtime_world_stock_issue_and_buyback_allowed(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.stock_issue_and_buyback_policy_raw_u8? == 0)
}
pub fn runtime_world_dividend_adjustment_allowed(state: &RuntimeState) -> Option<bool> {
Some(state.world_restore.dividend_policy_raw_u8? == 0)
}
pub fn runtime_company_annual_creditor_pressure_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualCreditorPressureState> {
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
let current_cash_plus_slot_12_total =
runtime_company_control_transfer_stat_value_f64(state, company_id, 0x12)
.and_then(runtime_round_f64_to_i64)
.and_then(|slot_12| {
runtime_company_control_transfer_stat_value_f64(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
)
.and_then(runtime_round_f64_to_i64)
.map(|current_cash| current_cash + slot_12)
});
let support_adjusted_share_price_scalar =
runtime_company_support_adjusted_share_price_scalar_f64(state, company_id)
.and_then(runtime_round_f64_to_i64);
let current_fuel_cost = runtime_company_stat_value_f64(
state,
company_id,
RuntimeCompanyStatSelector {
family_id: RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
slot_id: 0x09,
},
)
.and_then(runtime_round_f64_to_i64);
let recent_bad_net_profit_year_count = annual_finance_state
.trailing_full_year_net_profits
.iter()
.take(3)
.filter(|value| **value < -10_000)
.count() as u32;
let recent_peak_revenue = annual_finance_state
.trailing_full_year_revenues
.iter()
.take(3)
.copied()
.max();
let recent_three_year_net_profit_total =
if annual_finance_state.trailing_full_year_net_profits.len() >= 3 {
Some(
annual_finance_state
.trailing_full_year_net_profits
.iter()
.take(3)
.sum::<i64>(),
)
} else {
None
};
let pressure_ladder_cash_floor = recent_peak_revenue.map(|revenue| {
if revenue < 120_000 {
-600_000
} else if revenue < 230_000 {
-1_100_000
} else if revenue < 340_000 {
-1_600_000
} else {
-2_000_000
}
});
let support_adjusted_share_price_floor = Some(if recent_bad_net_profit_year_count == 3 {
20
} else {
15
});
let current_fuel_cost_floor = pressure_ladder_cash_floor.map(|floor| floor * 8 / 100);
let eligible_for_bankruptcy_branch = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_bankruptcy_allowed(state) == Some(true)
&& annual_finance_state
.years_since_last_bankruptcy
.is_some_and(|years| years >= 13)
&& annual_finance_state
.years_since_founding
.is_some_and(|years| years >= 4)
&& recent_bad_net_profit_year_count >= 2
&& current_cash_plus_slot_12_total
.zip(pressure_ladder_cash_floor)
.is_some_and(|(value, floor)| value <= floor)
&& support_adjusted_share_price_scalar
.zip(support_adjusted_share_price_floor)
.is_some_and(|(value, floor)| value >= floor)
&& current_fuel_cost
.zip(current_fuel_cost_floor)
.is_some_and(|(value, floor)| value <= floor)
&& recent_three_year_net_profit_total.is_some_and(|value| value <= -60_000);
Some(RuntimeCompanyAnnualCreditorPressureState {
company_id,
annual_mode_active: runtime_world_annual_finance_mode_active(state),
bankruptcy_allowed: runtime_world_bankruptcy_allowed(state),
years_since_last_bankruptcy: annual_finance_state.years_since_last_bankruptcy,
years_since_founding: annual_finance_state.years_since_founding,
recent_bad_net_profit_year_count,
recent_peak_revenue,
recent_three_year_net_profit_total,
pressure_ladder_cash_floor,
current_cash_plus_slot_12_total,
support_adjusted_share_price_floor,
support_adjusted_share_price_scalar,
current_fuel_cost,
current_fuel_cost_floor,
eligible_for_bankruptcy_branch,
})
}
pub fn runtime_world_absolute_counter(state: &RuntimeState) -> Option<u32> {
state.world_restore.absolute_counter_raw_u32
}
@ -3522,6 +3698,14 @@ mod tests {
issue_3a_value: None,
issue_37_multiplier_raw_u32: None,
issue_37_multiplier_value_f32_text: None,
stock_issue_and_buyback_policy_raw_u8: None,
bond_issue_and_repayment_policy_raw_u8: None,
bankruptcy_policy_raw_u8: None,
dividend_policy_raw_u8: None,
stock_issue_and_buyback_allowed: None,
bond_issue_and_repayment_allowed: None,
bankruptcy_allowed: None,
dividend_adjustment_allowed: None,
finance_neighborhood_candidates: Vec::new(),
economic_tuning_mirror_raw_u32: None,
economic_tuning_mirror_value_f32_text: None,
@ -4859,7 +5043,7 @@ mod tests {
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)
((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize
];
year_stat_family_qword_bits[(0x12 * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize] =
@ -5271,7 +5455,7 @@ mod tests {
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)
((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize
];
let write_year_value = |bits: &mut Vec<u64>, slot_id: u32, year_delta: u32, value: f64| {
@ -6626,6 +6810,132 @@ mod tests {
assert_eq!(runtime_company_annual_finance_state(&state, 99), None);
}
#[test]
fn derives_annual_creditor_pressure_from_rehosted_finance_owner_state() {
let mut year_stat_family_qword_bits = vec![
0u64;
(RUNTIME_COMPANY_STAT_SLOT_COUNT * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize
];
let write_current_value = |bits: &mut Vec<u64>, slot_id: u32, value: f64| {
let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize;
bits[index] = value.to_bits();
};
let write_prior_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_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x0d, -700_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x12, 0.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 80_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -115_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -110_000.0);
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),
partial_year_progress_raw_u8: Some(0x0c),
bankruptcy_policy_raw_u8: Some(0),
bankruptcy_allowed: Some(true),
..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 {
founding_year: 1841,
last_bankruptcy_year: 1832,
cached_share_price_raw_u32: 25.0f32.to_bits(),
year_stat_family_qword_bits,
..RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
assert_eq!(runtime_world_annual_finance_mode_active(&state), Some(true));
assert_eq!(runtime_world_bankruptcy_allowed(&state), Some(true));
let pressure_state = runtime_company_annual_creditor_pressure_state(&state, 7)
.expect("creditor pressure state");
assert_eq!(pressure_state.recent_bad_net_profit_year_count, 3);
assert_eq!(pressure_state.recent_peak_revenue, Some(100_000));
assert_eq!(
pressure_state.recent_three_year_net_profit_total,
Some(-65_000)
);
assert_eq!(pressure_state.pressure_ladder_cash_floor, Some(-600_000));
assert_eq!(
pressure_state.current_cash_plus_slot_12_total,
Some(-700_000)
);
assert_eq!(pressure_state.support_adjusted_share_price_floor, Some(20));
assert_eq!(pressure_state.support_adjusted_share_price_scalar, Some(25));
assert_eq!(pressure_state.current_fuel_cost, Some(-50_000));
assert_eq!(pressure_state.current_fuel_cost_floor, Some(-48_000));
assert!(pressure_state.eligible_for_bankruptcy_branch);
}
#[test]
fn reads_company_market_metrics_from_annual_finance_reader() {
let current_issue_calendar_word = 0x0101_0726;

View file

@ -133,6 +133,10 @@ const RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT: usize = 0x3b;
const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET: usize = 0x83;
const RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET: usize = 0xc1;
const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET: usize = 0x0bbf;
const RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET: usize = 0x4a83;
const RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET: usize = 0x4a87;
const RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET: usize = 0x4a8b;
const RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET: usize = 0x4a8f;
const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET: usize = 0x0bda;
const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS: [usize; 6] =
[0x0bde, 0x0be2, 0x0be6, 0x0bea, 0x0bee, 0x0bf2];
@ -1572,6 +1576,14 @@ pub struct SmpSaveWorldFinanceNeighborhoodProbe {
pub current_calendar_tuple_word_2_lane: SmpSaveDwordCandidate,
pub absolute_counter_lane: SmpSaveDwordCandidate,
pub absolute_counter_mirror_lane: SmpSaveDwordCandidate,
pub stock_policy_raw_u8: u8,
pub stock_policy_raw_hex: String,
pub bond_policy_raw_u8: u8,
pub bond_policy_raw_hex: String,
pub bankruptcy_policy_raw_u8: u8,
pub bankruptcy_policy_raw_hex: String,
pub dividend_policy_raw_u8: u8,
pub dividend_policy_raw_hex: String,
pub dword_candidates: Vec<SmpSaveDwordCandidate>,
pub evidence: Vec<String>,
}
@ -2256,6 +2268,14 @@ pub struct SmpLoadedWorldFinanceNeighborhoodState {
pub absolute_counter_raw_hex: String,
pub absolute_counter_mirror_raw_u32: u32,
pub absolute_counter_mirror_raw_hex: String,
pub stock_policy_raw_u8: u8,
pub stock_policy_raw_hex: String,
pub bond_policy_raw_u8: u8,
pub bond_policy_raw_hex: String,
pub bankruptcy_policy_raw_u8: u8,
pub bankruptcy_policy_raw_hex: String,
pub dividend_policy_raw_u8: u8,
pub dividend_policy_raw_hex: String,
pub labels: Vec<String>,
pub relative_offsets: Vec<usize>,
pub relative_offset_hex: Vec<String>,
@ -3500,6 +3520,14 @@ fn derive_loaded_world_finance_neighborhood_state_from_probe(
absolute_counter_raw_hex: probe.absolute_counter_lane.raw_u32_hex.clone(),
absolute_counter_mirror_raw_u32: probe.absolute_counter_mirror_lane.raw_u32,
absolute_counter_mirror_raw_hex: probe.absolute_counter_mirror_lane.raw_u32_hex.clone(),
stock_policy_raw_u8: probe.stock_policy_raw_u8,
stock_policy_raw_hex: probe.stock_policy_raw_hex.clone(),
bond_policy_raw_u8: probe.bond_policy_raw_u8,
bond_policy_raw_hex: probe.bond_policy_raw_hex.clone(),
bankruptcy_policy_raw_u8: probe.bankruptcy_policy_raw_u8,
bankruptcy_policy_raw_hex: probe.bankruptcy_policy_raw_hex.clone(),
dividend_policy_raw_u8: probe.dividend_policy_raw_u8,
dividend_policy_raw_hex: probe.dividend_policy_raw_hex.clone(),
labels: probe
.dword_candidates
.iter()
@ -9100,6 +9128,22 @@ fn parse_save_world_finance_neighborhood_probe(
"absolute_calendar_counter_mirror",
RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET,
)?;
let stock_policy_raw_u8 = read_u8_at(
bytes,
payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET,
)?;
let bond_policy_raw_u8 = read_u8_at(
bytes,
payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET,
)?;
let bankruptcy_policy_raw_u8 = read_u8_at(
bytes,
payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET,
)?;
let dividend_policy_raw_u8 = read_u8_at(
bytes,
payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET,
)?;
let dword_candidates =
build_save_world_finance_neighborhood_candidates(bytes, payload_offset)?;
@ -9119,6 +9163,14 @@ fn parse_save_world_finance_neighborhood_probe(
current_calendar_tuple_word_2_lane,
absolute_counter_lane,
absolute_counter_mirror_lane,
stock_policy_raw_u8,
stock_policy_raw_hex: format!("0x{stock_policy_raw_u8:02x}"),
bond_policy_raw_u8,
bond_policy_raw_hex: format!("0x{bond_policy_raw_u8:02x}"),
bankruptcy_policy_raw_u8,
bankruptcy_policy_raw_hex: format!("0x{bankruptcy_policy_raw_u8:02x}"),
dividend_policy_raw_u8,
dividend_policy_raw_hex: format!("0x{dividend_policy_raw_u8:02x}"),
dword_candidates,
evidence: vec![
format!(
@ -9133,6 +9185,13 @@ fn parse_save_world_finance_neighborhood_probe(
RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET,
RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET
),
format!(
"payload +0x{:x}/+0x{:x}/+0x{:x}/+0x{:x} carry the stock, bond, bankruptcy, and dividend finance-policy bytes mirrored from scenario offsets 0x4a87/0x4a8b/0x4a8f/0x4a93",
RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET,
RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET,
RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET,
RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET
),
"finance-neighborhood candidates cover the fixed dword strip around the grounded world calendar tuple, absolute-counter, selection-context, and issue-0x37 lanes so broader finance reader closure can build on one rehosted owner surface.".to_string(),
],
});
@ -15868,6 +15927,10 @@ mod tests {
bytes[payload_offset + relative_offset..payload_offset + relative_offset + 4]
.copy_from_slice(&((index as u32) + 1).to_le_bytes());
}
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET] = 1;
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET] = 2;
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET] = 3;
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET] = 4;
let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN;
bytes[next_chunk_offset..next_chunk_offset + 4]
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes());
@ -15897,6 +15960,14 @@ mod tests {
assert_eq!(probe.packed_year_word_raw_hex, "0x0001");
assert_eq!(probe.partial_year_progress_raw_u8, 0);
assert_eq!(probe.partial_year_progress_raw_hex, "0x00");
assert_eq!(probe.stock_policy_raw_u8, 1);
assert_eq!(probe.stock_policy_raw_hex, "0x01");
assert_eq!(probe.bond_policy_raw_u8, 2);
assert_eq!(probe.bond_policy_raw_hex, "0x02");
assert_eq!(probe.bankruptcy_policy_raw_u8, 3);
assert_eq!(probe.bankruptcy_policy_raw_hex, "0x03");
assert_eq!(probe.dividend_policy_raw_u8, 4);
assert_eq!(probe.dividend_policy_raw_hex, "0x04");
assert_eq!(probe.current_calendar_tuple_word_lane.value_i32, 1);
assert_eq!(
probe.current_calendar_tuple_word_2_lane.relative_offset_hex,

View file

@ -1,8 +1,8 @@
use serde::{Deserialize, Serialize};
use crate::{
CalendarPoint, RuntimeState, runtime_company_annual_finance_state,
runtime_company_unassigned_share_pool,
CalendarPoint, RuntimeState, runtime_company_annual_creditor_pressure_state,
runtime_company_annual_finance_state, runtime_company_unassigned_share_pool,
};
fn raw_u32_to_f32_text(raw: u32) -> String {
@ -50,6 +50,14 @@ pub struct RuntimeSummary {
pub world_restore_issue_3a_value: Option<u32>,
pub world_restore_issue_37_multiplier_raw_u32: Option<u32>,
pub world_restore_issue_37_multiplier_value_f32_text: Option<String>,
pub world_restore_stock_issue_and_buyback_policy_raw_u8: Option<u8>,
pub world_restore_bond_issue_and_repayment_policy_raw_u8: Option<u8>,
pub world_restore_bankruptcy_policy_raw_u8: Option<u8>,
pub world_restore_dividend_policy_raw_u8: Option<u8>,
pub world_restore_stock_issue_and_buyback_allowed: Option<bool>,
pub world_restore_bond_issue_and_repayment_allowed: Option<bool>,
pub world_restore_bankruptcy_allowed: Option<bool>,
pub world_restore_dividend_adjustment_allowed: Option<bool>,
pub world_restore_finance_neighborhood_count: usize,
pub world_restore_finance_neighborhood_labels: Vec<String>,
pub world_restore_economic_tuning_mirror_raw_u32: Option<u32>,
@ -87,6 +95,16 @@ pub struct RuntimeSummary {
pub selected_company_current_issue_age_absolute_counter_delta: Option<i64>,
pub selected_company_chairman_bonus_year: Option<u32>,
pub selected_company_chairman_bonus_amount: Option<i32>,
pub selected_company_creditor_pressure_recent_bad_net_profit_year_count: Option<u32>,
pub selected_company_creditor_pressure_recent_peak_revenue: Option<i64>,
pub selected_company_creditor_pressure_recent_three_year_net_profit_total: Option<i64>,
pub selected_company_creditor_pressure_cash_floor: Option<i64>,
pub selected_company_creditor_pressure_cash_plus_slot_12_total: Option<i64>,
pub selected_company_creditor_pressure_share_price_floor: Option<i64>,
pub selected_company_creditor_pressure_share_price_scalar: Option<i64>,
pub selected_company_creditor_pressure_current_fuel_cost: Option<i64>,
pub selected_company_creditor_pressure_current_fuel_cost_floor: Option<i64>,
pub selected_company_creditor_pressure_eligible_for_bankruptcy_branch: Option<bool>,
pub player_count: usize,
pub chairman_profile_count: usize,
pub active_chairman_profile_count: usize,
@ -176,6 +194,10 @@ impl RuntimeSummary {
let selected_company_annual_finance_state = state
.selected_company_id
.and_then(|company_id| runtime_company_annual_finance_state(state, company_id));
let selected_company_creditor_pressure_state =
state.selected_company_id.and_then(|company_id| {
runtime_company_annual_creditor_pressure_state(state, company_id)
});
Self {
calendar: state.calendar,
calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(),
@ -255,6 +277,24 @@ impl RuntimeSummary {
.world_restore
.issue_37_multiplier_value_f32_text
.clone(),
world_restore_stock_issue_and_buyback_policy_raw_u8: state
.world_restore
.stock_issue_and_buyback_policy_raw_u8,
world_restore_bond_issue_and_repayment_policy_raw_u8: state
.world_restore
.bond_issue_and_repayment_policy_raw_u8,
world_restore_bankruptcy_policy_raw_u8: state.world_restore.bankruptcy_policy_raw_u8,
world_restore_dividend_policy_raw_u8: state.world_restore.dividend_policy_raw_u8,
world_restore_stock_issue_and_buyback_allowed: state
.world_restore
.stock_issue_and_buyback_allowed,
world_restore_bond_issue_and_repayment_allowed: state
.world_restore
.bond_issue_and_repayment_allowed,
world_restore_bankruptcy_allowed: state.world_restore.bankruptcy_allowed,
world_restore_dividend_adjustment_allowed: state
.world_restore
.dividend_adjustment_allowed,
world_restore_finance_neighborhood_count: state
.world_restore
.finance_neighborhood_candidates
@ -372,6 +412,45 @@ impl RuntimeSummary {
selected_company_chairman_bonus_amount: selected_company_market_state
.map(|market_state| market_state.chairman_bonus_amount)
.filter(|amount| *amount != 0),
selected_company_creditor_pressure_recent_bad_net_profit_year_count:
selected_company_creditor_pressure_state
.as_ref()
.map(|pressure_state| pressure_state.recent_bad_net_profit_year_count),
selected_company_creditor_pressure_recent_peak_revenue:
selected_company_creditor_pressure_state
.as_ref()
.and_then(|pressure_state| pressure_state.recent_peak_revenue),
selected_company_creditor_pressure_recent_three_year_net_profit_total:
selected_company_creditor_pressure_state
.as_ref()
.and_then(|pressure_state| pressure_state.recent_three_year_net_profit_total),
selected_company_creditor_pressure_cash_floor: selected_company_creditor_pressure_state
.as_ref()
.and_then(|pressure_state| pressure_state.pressure_ladder_cash_floor),
selected_company_creditor_pressure_cash_plus_slot_12_total:
selected_company_creditor_pressure_state
.as_ref()
.and_then(|pressure_state| pressure_state.current_cash_plus_slot_12_total),
selected_company_creditor_pressure_share_price_floor:
selected_company_creditor_pressure_state
.as_ref()
.and_then(|pressure_state| pressure_state.support_adjusted_share_price_floor),
selected_company_creditor_pressure_share_price_scalar:
selected_company_creditor_pressure_state
.as_ref()
.and_then(|pressure_state| pressure_state.support_adjusted_share_price_scalar),
selected_company_creditor_pressure_current_fuel_cost:
selected_company_creditor_pressure_state
.as_ref()
.and_then(|pressure_state| pressure_state.current_fuel_cost),
selected_company_creditor_pressure_current_fuel_cost_floor:
selected_company_creditor_pressure_state
.as_ref()
.and_then(|pressure_state| pressure_state.current_fuel_cost_floor),
selected_company_creditor_pressure_eligible_for_bankruptcy_branch:
selected_company_creditor_pressure_state
.as_ref()
.map(|pressure_state| pressure_state.eligible_for_bankruptcy_branch),
player_count: state.players.len(),
chairman_profile_count: state.chairman_profiles.len(),
active_chairman_profile_count: state
@ -1217,6 +1296,14 @@ mod tests {
issue_3a_value: Some(4),
issue_37_multiplier_raw_u32: Some(0x3d75c28f),
issue_37_multiplier_value_f32_text: Some("0.06".to_string()),
stock_issue_and_buyback_policy_raw_u8: Some(0),
bond_issue_and_repayment_policy_raw_u8: Some(1),
bankruptcy_policy_raw_u8: Some(0),
dividend_policy_raw_u8: Some(1),
stock_issue_and_buyback_allowed: Some(true),
bond_issue_and_repayment_allowed: Some(false),
bankruptcy_allowed: Some(true),
dividend_adjustment_allowed: Some(false),
economic_tuning_mirror_raw_u32: Some(0x3f46dff5),
economic_tuning_mirror_value_f32_text: Some("0.7766201".to_string()),
economic_tuning_lane_raw_u32: vec![
@ -1301,6 +1388,29 @@ mod tests {
summary.world_restore_issue_37_multiplier_raw_u32,
Some(0x3d75c28f)
);
assert_eq!(
summary.world_restore_stock_issue_and_buyback_policy_raw_u8,
Some(0)
);
assert_eq!(
summary.world_restore_bond_issue_and_repayment_policy_raw_u8,
Some(1)
);
assert_eq!(summary.world_restore_bankruptcy_policy_raw_u8, Some(0));
assert_eq!(summary.world_restore_dividend_policy_raw_u8, Some(1));
assert_eq!(
summary.world_restore_stock_issue_and_buyback_allowed,
Some(true)
);
assert_eq!(
summary.world_restore_bond_issue_and_repayment_allowed,
Some(false)
);
assert_eq!(summary.world_restore_bankruptcy_allowed, Some(true));
assert_eq!(
summary.world_restore_dividend_adjustment_allowed,
Some(false)
);
assert_eq!(
summary
.world_restore_issue_37_multiplier_value_f32_text
@ -2177,4 +2287,153 @@ mod tests {
assert_eq!(summary.selected_company_chairman_bonus_year, Some(1842));
assert_eq!(summary.selected_company_chairman_bonus_amount, Some(750));
}
#[test]
fn summarizes_selected_company_creditor_pressure_branch_state() {
let mut year_stat_family_qword_bits = vec![
0u64;
((crate::RUNTIME_COMPANY_STAT_SLOT_COUNT + 2)
* crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)
as usize
];
let write_current_value = |bits: &mut Vec<u64>, slot_id: u32, value: f64| {
let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize;
bits[index] = value.to_bits();
};
let write_prior_year_value =
|bits: &mut Vec<u64>, slot_id: u32, year_delta: u32, value: f64| {
let index =
(slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN + year_delta) as usize;
bits[index] = value.to_bits();
};
write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x0d, -700_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x12, 0.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 100_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 90_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 80_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -115_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -110_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -110_000.0);
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),
partial_year_progress_raw_u8: Some(0x0c),
bankruptcy_policy_raw_u8: Some(0),
bankruptcy_allowed: Some(true),
..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: 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(7),
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,
crate::RuntimeCompanyMarketState {
founding_year: 1841,
last_bankruptcy_year: 1832,
cached_share_price_raw_u32: 25.0f32.to_bits(),
year_stat_family_qword_bits,
..crate::RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(
summary.selected_company_creditor_pressure_recent_bad_net_profit_year_count,
Some(3)
);
assert_eq!(
summary.selected_company_creditor_pressure_recent_peak_revenue,
Some(100_000)
);
assert_eq!(
summary.selected_company_creditor_pressure_recent_three_year_net_profit_total,
Some(-65_000)
);
assert_eq!(
summary.selected_company_creditor_pressure_cash_floor,
Some(-600_000)
);
assert_eq!(
summary.selected_company_creditor_pressure_cash_plus_slot_12_total,
Some(-700_000)
);
assert_eq!(
summary.selected_company_creditor_pressure_share_price_floor,
Some(20)
);
assert_eq!(
summary.selected_company_creditor_pressure_share_price_scalar,
Some(25)
);
assert_eq!(
summary.selected_company_creditor_pressure_current_fuel_cost,
Some(-50_000)
);
assert_eq!(
summary.selected_company_creditor_pressure_current_fuel_cost_floor,
Some(-48_000)
);
assert_eq!(
summary.selected_company_creditor_pressure_eligible_for_bankruptcy_branch,
Some(true)
);
}
}

View file

@ -134,7 +134,9 @@ The highest-value next passes are now:
and last bankruptcy for later annual finance-policy rehosting; live bond-slot count now travels through that same owned annual-finance
state for the stock-capital branch gate, and the grounded bond table now also contributes both
the largest live bond principal and the chosen highest-coupon live bond principal into that same
owner-state surface
owner-state surface; the same fixed-world save block now also carries the raw stock, bond,
bankruptcy, and dividend finance-policy bytes, and the first annual creditor-pressure branch now
executes as a pure runtime reader over that owner state instead of remaining atlas-only
- 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

@ -225,7 +225,10 @@ owned company market and annual-finance state, matching the stock-capital branch
at least two live bonds. The same grounded bond table now also contributes both the largest live
bond principal and the chosen highest-coupon live bond principal into owned company market and
annual-finance state, so later stock-capital gates can extend a rehosted owner-state seam instead
of guessing another finance leaf.
of guessing another finance leaf. The same fixed-world save block now also carries the raw stock,
bond, bankruptcy, and dividend finance-policy bytes, and the earliest annual creditor-pressure
bankruptcy branch now runs as a pure runtime reader over owned annual-finance state, support-
adjusted share price, and those policy bytes rather than staying in atlas prose only.
## Why This Boundary