Rehost annual bankruptcy service branches

This commit is contained in:
Jan Petykiewicz 2026-04-18 01:38:42 -07:00
commit b87216a556
4 changed files with 366 additions and 6 deletions

View file

@ -105,9 +105,10 @@ full annual dividend adjustment branch over owned current cash, public float, cu
building-growth policy, and recent profit history instead of leaving that policy on shell-side building-growth policy, and recent profit history instead of leaving that policy on shell-side
dialog notes. `simulation_service_periodic_boundary_work` is now beginning to use that same owner dialog notes. `simulation_service_periodic_boundary_work` is now beginning to use that same owner
surface too: the runtime chooses one annual-finance action per active company and already commits surface too: the runtime chooses one annual-finance action per active company and already commits
the shellless dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches by the shellless creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment,
mutating owned dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot stock-repurchase, stock-issue, and bond-issue branches by mutating owned company activity,
state instead of stopping at reader-only diagnostics. dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state instead
of stopping at reader-only diagnostics.
The same seam now also carries the fixed-world building-density growth setting plus the linked The same seam now also carries the fixed-world building-density growth setting plus the linked
chairman personality byte, which is enough to run the annual stock-repurchase gate as another chairman personality byte, which is enough to run the annual stock-repurchase gate as another
pure reader over owned save-native state instead of a guessed finance-side approximation. pure reader over owned save-native state instead of a guessed finance-side approximation.

View file

@ -306,6 +306,45 @@ fn service_post_company_stat_delta(
} }
} }
fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool {
let Some(bankruptcy_year) = state
.world_restore
.packed_year_word_raw_u16
.map(u32::from)
.or_else(|| Some(state.calendar.year))
else {
return false;
};
let mut company_mutated = false;
if let Some(company) = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
{
company.current_cash = 0;
company.debt = 0;
company.active = false;
if state.selected_company_id == Some(company_id) {
state.selected_company_id = None;
}
company_mutated = true;
}
if let Some(market_state) = state.service_state.company_market_state.get_mut(&company_id) {
market_state.last_bankruptcy_year = bankruptcy_year;
market_state.bond_count = 0;
market_state.largest_live_bond_principal = None;
market_state.highest_coupon_live_bond_principal = None;
market_state.live_bond_slots.clear();
company_mutated = true;
}
let retired_company_ids = vec![company_id];
retire_matching_trains(&mut state.trains, Some(&retired_company_ids), None, None);
company_mutated
}
fn service_company_annual_finance_policy( fn service_company_annual_finance_policy(
state: &mut RuntimeState, state: &mut RuntimeState,
service_events: &mut Vec<ServiceEvent>, service_events: &mut Vec<ServiceEvent>,
@ -369,6 +408,13 @@ fn service_company_annual_finance_policy(
mutated_company_ids.insert(company_id); mutated_company_ids.insert(company_id);
} }
} }
crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy
| crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback => {
if service_apply_company_bankruptcy(state, company_id) {
applied_effect_count += 1;
mutated_company_ids.insert(company_id);
}
}
crate::RuntimeCompanyAnnualFinancePolicyAction::StockIssue => { crate::RuntimeCompanyAnnualFinancePolicyAction::StockIssue => {
let Some(issue_state) = runtime_company_annual_stock_issue_state(state, company_id) let Some(issue_state) = runtime_company_annual_stock_issue_state(state, company_id)
else { else {
@ -2952,6 +2998,317 @@ mod tests {
); );
} }
#[test]
fn periodic_boundary_applies_creditor_pressure_bankruptcy_from_annual_finance_policy() {
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,
crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
-200_000.0,
);
write_current_value(&mut year_stat_family_qword_bits, 0x12, -500_000.0);
write_current_value(&mut year_stat_family_qword_bits, 0x09, -50_000.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, 95_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -125_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, -115_000.0);
let mut state = RuntimeState {
calendar: crate::CalendarPoint {
year: 1845,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: crate::RuntimeSaveProfileState::default(),
world_restore: crate::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),
..crate::RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany {
company_id: 31,
controller_kind: crate::RuntimeCompanyControllerKind::Unknown,
current_cash: 0,
debt: 200_000,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: crate::RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
}],
selected_company_id: Some(31),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![crate::RuntimeTrain {
train_id: 88,
owner_company_id: 31,
territory_id: None,
locomotive_name: Some("Mikado".to_string()),
active: true,
retired: false,
}],
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: crate::RuntimeServiceState {
company_market_state: BTreeMap::from([(
31,
crate::RuntimeCompanyMarketState {
outstanding_shares: 10_000,
bond_count: 1,
largest_live_bond_principal: Some(500_000),
highest_coupon_live_bond_principal: Some(500_000),
cached_share_price_raw_u32: 25.0f32.to_bits(),
founding_year: 1841,
last_bankruptcy_year: 1832,
year_stat_family_qword_bits,
live_bond_slots: vec![crate::RuntimeCompanyBondSlot {
slot_index: 0,
principal: 500_000,
coupon_rate_raw_u32: 0.08f32.to_bits(),
}],
..crate::RuntimeCompanyMarketState::default()
},
)]),
..crate::RuntimeServiceState::default()
},
};
let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary)
.expect("periodic boundary should apply creditor-pressure bankruptcy");
assert_eq!(
state.service_state.annual_finance_last_actions.get(&31),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::CreditorPressureBankruptcy)
);
assert!(!state.companies[0].active);
assert_eq!(state.companies[0].current_cash, 0);
assert_eq!(state.companies[0].debt, 0);
assert_eq!(state.selected_company_id, None);
assert!(state.trains[0].retired);
assert_eq!(
state.service_state.company_market_state[&31].last_bankruptcy_year,
1845
);
assert_eq!(state.service_state.company_market_state[&31].bond_count, 0);
assert!(
state.service_state.company_market_state[&31]
.live_bond_slots
.is_empty()
);
assert!(
result
.service_events
.iter()
.any(|event| event.kind == "annual_finance_policy"
&& event.applied_effect_count == 1
&& event.mutated_company_ids == vec![31])
);
}
#[test]
fn periodic_boundary_applies_deep_distress_bankruptcy_fallback_from_annual_finance_policy() {
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,
crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
-350_000.0,
);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 1, 10_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 2, 15_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x01, 3, 12_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 1, -35_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 2, -38_000.0);
write_prior_year_value(&mut year_stat_family_qword_bits, 0x09, 3, -33_000.0);
let mut state = RuntimeState {
calendar: crate::CalendarPoint {
year: 1845,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: crate::RuntimeSaveProfileState::default(),
world_restore: crate::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),
..crate::RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany {
company_id: 32,
controller_kind: crate::RuntimeCompanyControllerKind::Unknown,
current_cash: 0,
debt: 50_000,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: crate::RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
}],
selected_company_id: Some(32),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: vec![crate::RuntimeTrain {
train_id: 89,
owner_company_id: 32,
territory_id: None,
locomotive_name: Some("Orca".to_string()),
active: true,
retired: false,
}],
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: crate::RuntimeServiceState {
company_market_state: BTreeMap::from([(
32,
crate::RuntimeCompanyMarketState {
outstanding_shares: 8_000,
bond_count: 1,
largest_live_bond_principal: Some(250_000),
highest_coupon_live_bond_principal: Some(250_000),
founding_year: 1841,
last_bankruptcy_year: 1840,
year_stat_family_qword_bits,
live_bond_slots: vec![crate::RuntimeCompanyBondSlot {
slot_index: 0,
principal: 250_000,
coupon_rate_raw_u32: 0.07f32.to_bits(),
}],
..crate::RuntimeCompanyMarketState::default()
},
)]),
..crate::RuntimeServiceState::default()
},
};
let result = execute_step_command(&mut state, &StepCommand::ServicePeriodicBoundary)
.expect("periodic boundary should apply deep-distress bankruptcy fallback");
assert_eq!(
state.service_state.annual_finance_last_actions.get(&32),
Some(&crate::RuntimeCompanyAnnualFinancePolicyAction::DeepDistressBankruptcyFallback)
);
assert!(!state.companies[0].active);
assert_eq!(state.companies[0].current_cash, 0);
assert_eq!(state.companies[0].debt, 0);
assert_eq!(state.selected_company_id, None);
assert!(state.trains[0].retired);
assert_eq!(
state.service_state.company_market_state[&32].last_bankruptcy_year,
1845
);
assert_eq!(state.service_state.company_market_state[&32].bond_count, 0);
assert!(
state.service_state.company_market_state[&32]
.live_bond_slots
.is_empty()
);
assert!(
result
.service_events
.iter()
.any(|event| event.kind == "annual_finance_policy"
&& event.applied_effect_count == 1
&& event.mutated_company_ids == vec![32])
);
}
#[test] #[test]
fn applies_company_effects_for_specific_targets() { fn applies_company_effects_for_specific_targets() {
let mut state = RuntimeState { let mut state = RuntimeState {

View file

@ -149,7 +149,8 @@ The highest-value next passes are now:
growth setting plus the linked chairman personality byte, which is enough to run the annual growth setting plus the linked chairman personality byte, which is enough to run the annual
stock-repurchase gate headlessly as another pure reader; periodic boundary service now also stock-repurchase gate headlessly as another pure reader; periodic boundary service now also
chooses one annual-finance action per active company and already commits the shellless chooses one annual-finance action per active company and already commits the shellless
dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches against owned runtime state creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase,
stock-issue, and bond-issue branches against owned runtime state
- the project rule on the remaining closure work is now explicit too: when one runtime-facing field - 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 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 instead of guessing another derived leaf field from neighboring raw offsets

View file

@ -244,8 +244,9 @@ save/runtime state instead of another threshold-only note. The stock-capital iss
rides that same seam too, with share-pressure, cooldown, and price-to-book gate state exposed as rides that same seam too, with share-pressure, cooldown, and price-to-book gate state exposed as
normal runtime readers. Periodic boundary service now also uses that owner seam as a real chooser: normal runtime readers. Periodic boundary service now also uses that owner seam as a real chooser:
the runtime selects one annual-finance action per active company and already commits the shellless the runtime selects one annual-finance action per active company and already commits the shellless
dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches directly into owned creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase,
dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state. stock-issue, and bond-issue branches directly into owned dividend, company stat-post,
outstanding-share, issue-calendar, live bond-slot, and company activity state.
## Why This Boundary ## Why This Boundary