From d57deb0e13908a2950a32647f6d832bbf2b8f685 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 21:24:53 -0700 Subject: [PATCH] Rehost company largest live bond principal --- README.md | 5 ++- crates/rrt-fixtures/src/schema.rs | 10 ++++++ crates/rrt-runtime/src/import.rs | 3 ++ crates/rrt-runtime/src/lib.rs | 25 +++++++------- crates/rrt-runtime/src/runtime.rs | 52 ++++++++++++++++++++++------- crates/rrt-runtime/src/smp.rs | 55 ++++++++++++++++++++++++++++--- crates/rrt-runtime/src/summary.rs | 12 +++++-- docs/README.md | 3 +- docs/runtime-rehost-plan.md | 4 ++- 9 files changed, 135 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 6c2a73e..aec2098 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,10 @@ instead of leaving that logic spread across summary helpers. The same annual-fin derives elapsed years since founding, last dividend, and last bankruptcy from the runtime calendar, which lines up directly with the grounded annual finance-policy gates in the atlas. Live bond-slot count is now carried through the same owned company market and annual-finance state too, which -matches the stock-capital branch gate that requires at least two live bonds. A checked-in +matches the stock-capital branch gate that requires at least two live bonds. The same grounded +bond table now also contributes the largest 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 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 diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index fc52439..bd16868 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -94,6 +94,8 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub selected_company_bond_count: Option, #[serde(default)] + pub selected_company_largest_live_bond_principal: Option, + #[serde(default)] pub selected_company_assigned_share_pool: Option, #[serde(default)] pub selected_company_unassigned_share_pool: Option, @@ -619,6 +621,14 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(value) = self.selected_company_largest_live_bond_principal { + if actual.selected_company_largest_live_bond_principal != Some(value) { + mismatches.push(format!( + "selected_company_largest_live_bond_principal mismatch: expected {value}, got {:?}", + actual.selected_company_largest_live_bond_principal + )); + } + } if let Some(value) = self.selected_company_assigned_share_pool { if actual.selected_company_assigned_share_pool != Some(value) { mismatches.push(format!( diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index a3a3994..d7ac8d8 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -5042,6 +5042,7 @@ mod tests { market_state: Some(crate::RuntimeCompanyMarketState { outstanding_shares: 20_000, bond_count: 2, + largest_live_bond_principal: Some(500_000), mutable_support_scalar_raw_u32: 0x3f99999a, young_company_support_scalar_raw_u32: 0x42700000, support_progress_word: 12, @@ -5089,6 +5090,7 @@ mod tests { market_state: Some(crate::RuntimeCompanyMarketState { outstanding_shares: 18_000, bond_count: 1, + largest_live_bond_principal: Some(300_000), mutable_support_scalar_raw_u32: 0x3f4ccccd, young_company_support_scalar_raw_u32: 0x42580000, support_progress_word: 9, @@ -6379,6 +6381,7 @@ mod tests { crate::RuntimeCompanyMarketState { outstanding_shares: 30_000, bond_count: 3, + largest_live_bond_principal: Some(750_000), mutable_support_scalar_raw_u32: 0x3f19999a, young_company_support_scalar_raw_u32: 0x42580000, support_progress_word: 8, diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 5815a6e..ded9d6c 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -44,6 +44,8 @@ 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_INVESTOR_CONFIDENCE, RuntimeCargoCatalogEntry, RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyAnnualFinanceState, @@ -51,19 +53,16 @@ pub use runtime::{ 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_STAT_FAMILY_CONTROL_TRANSFER, RUNTIME_COMPANY_STAT_SLOT_BOOK_VALUE_PER_SHARE, - RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE, - runtime_company_annual_finance_state, runtime_company_assigned_share_pool, - runtime_company_market_value, runtime_company_stat_value, + 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_stat_value, runtime_company_unassigned_share_pool, runtime_world_issue_state, }; pub use smp::{ diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 02249f2..28cfb81 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -55,6 +55,8 @@ pub struct RuntimeCompanyMarketState { #[serde(default)] pub bond_count: u8, #[serde(default)] + pub largest_live_bond_principal: Option, + #[serde(default)] pub mutable_support_scalar_raw_u32: u32, #[serde(default)] pub young_company_support_scalar_raw_u32: u32, @@ -99,6 +101,8 @@ pub struct RuntimeCompanyAnnualFinanceState { pub company_id: u32, pub outstanding_shares: u32, pub bond_count: u8, + #[serde(default)] + pub largest_live_bond_principal: Option, pub assigned_share_pool: u32, pub unassigned_share_pool: u32, #[serde(default)] @@ -356,6 +360,7 @@ pub enum RuntimeCompanyMetric { pub enum RuntimeCompanyMarketMetric { OutstandingShares, BondCount, + LargestLiveBondPrincipal, AssignedSharePool, UnassignedSharePool, CachedSharePrice, @@ -1855,7 +1860,10 @@ pub fn runtime_company_stat_value( company_id: u32, selector: RuntimeCompanyStatSelector, ) -> Option { - let company = state.companies.iter().find(|company| company.company_id == company_id)?; + 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) @@ -1868,7 +1876,10 @@ pub fn runtime_company_stat_value( } } -pub fn runtime_world_issue_state(state: &RuntimeState, issue_id: u32) -> Option { +pub fn runtime_world_issue_state( + state: &RuntimeState, + issue_id: u32, +) -> Option { match issue_id { RUNTIME_WORLD_ISSUE_INVESTOR_CONFIDENCE => Some(RuntimeWorldIssueState { issue_id, @@ -1894,10 +1905,7 @@ pub fn runtime_company_unassigned_share_pool(state: &RuntimeState, company_id: u } pub fn runtime_company_assigned_share_pool(state: &RuntimeState, company_id: u32) -> Option { - state - .service_state - .company_market_state - .get(&company_id)?; + state.service_state.company_market_state.get(&company_id)?; Some( state .chairman_profiles @@ -1920,14 +1928,13 @@ pub fn runtime_company_annual_finance_state( state.calendar.year, market_state.last_bankruptcy_year, ); - let years_since_last_dividend = derive_runtime_company_elapsed_years( - state.calendar.year, - market_state.last_dividend_year, - ); + let years_since_last_dividend = + derive_runtime_company_elapsed_years(state.calendar.year, market_state.last_dividend_year); Some(RuntimeCompanyAnnualFinanceState { company_id, outstanding_shares: market_state.outstanding_shares, bond_count: market_state.bond_count, + largest_live_bond_principal: market_state.largest_live_bond_principal, assigned_share_pool, unassigned_share_pool, cached_share_price: rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32), @@ -1959,6 +1966,9 @@ pub fn runtime_company_market_value( Some(annual_finance_state.outstanding_shares as i64) } RuntimeCompanyMarketMetric::BondCount => Some(annual_finance_state.bond_count as i64), + RuntimeCompanyMarketMetric::LargestLiveBondPrincipal => annual_finance_state + .largest_live_bond_principal + .map(|value| value as i64), RuntimeCompanyMarketMetric::AssignedSharePool => { Some(annual_finance_state.assigned_share_pool as i64) } @@ -4137,7 +4147,10 @@ mod tests { }, }; - assert_eq!(runtime_company_unassigned_share_pool(&state, 4), Some(4_500)); + assert_eq!( + runtime_company_unassigned_share_pool(&state, 4), + Some(4_500) + ); assert_eq!(runtime_company_unassigned_share_pool(&state, 99), None); } @@ -4229,6 +4242,7 @@ mod tests { RuntimeCompanyMarketState { outstanding_shares: 20_000, bond_count: 3, + largest_live_bond_principal: Some(650_000), cached_share_price_raw_u32: 0x42200000, chairman_salary_baseline: 24, chairman_salary_current: 30, @@ -4255,6 +4269,7 @@ mod tests { company_id: 4, outstanding_shares: 20_000, bond_count: 3, + largest_live_bond_principal: Some(650_000), assigned_share_pool: 15_500, unassigned_share_pool: 4_500, cached_share_price: Some(40), @@ -4353,6 +4368,7 @@ mod tests { RuntimeCompanyMarketState { outstanding_shares: 20_000, bond_count: 2, + largest_live_bond_principal: Some(500_000), cached_share_price_raw_u32: 0x42200000, chairman_salary_baseline: 18, chairman_salary_current: 27, @@ -4378,12 +4394,24 @@ mod tests { runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::BondCount), Some(2) ); + assert_eq!( + runtime_company_market_value( + &state, + 7, + RuntimeCompanyMarketMetric::LargestLiveBondPrincipal + ), + Some(500_000) + ); assert_eq!( runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::AssignedSharePool), Some(12_000) ); assert_eq!( - runtime_company_market_value(&state, 7, RuntimeCompanyMarketMetric::UnassignedSharePool), + runtime_company_market_value( + &state, + 7, + RuntimeCompanyMarketMetric::UnassignedSharePool + ), Some(8_000) ); assert_eq!( diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 98da2b9..57c2958 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -2329,6 +2329,8 @@ pub struct SmpSaveCompanyRecordAnalysisEntry { pub debt: u64, pub bond_count: u8, #[serde(default)] + pub largest_live_bond_principal: Option, + #[serde(default)] pub available_track_laying_capacity: Option, pub company_value_scalar_f32: f32, pub cached_share_support_scalar_f32: f32, @@ -3002,6 +3004,8 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( &bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET, )?; + let largest_live_bond_principal = + parse_save_company_largest_live_bond_principal(&bytes, record_offset)?; let available_track_laying_capacity = parse_save_company_available_track_laying_capacity(&bytes, record_offset)?; let company_value_scalar_f32 = read_f32_at( @@ -3101,6 +3105,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( outstanding_shares, debt, bond_count, + largest_live_bond_principal, available_track_laying_capacity, company_value_scalar_f32, cached_share_support_scalar_f32, @@ -3582,6 +3587,8 @@ 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 largest_live_bond_principal = + parse_save_company_largest_live_bond_principal(bytes, record_offset)?; let available_track_laying_capacity = parse_save_company_available_track_laying_capacity(bytes, record_offset)?; let mutable_support_scalar_raw_u32 = read_u32_at( @@ -3696,6 +3703,7 @@ fn parse_save_company_roster_probe( market_state: Some(RuntimeCompanyMarketState { outstanding_shares, bond_count, + largest_live_bond_principal, mutable_support_scalar_raw_u32, young_company_support_scalar_raw_u32, support_progress_word, @@ -3874,6 +3882,29 @@ fn parse_save_company_total_debt(bytes: &[u8], record_offset: usize) -> Option Option> { + let bond_count = + read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize; + let mut largest_live_principal: Option = 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_available_track_laying_capacity( bytes: &[u8], record_offset: usize, @@ -15490,10 +15521,16 @@ mod tests { ); assert_eq!(probe.dword_candidates[9].relative_offset_hex, "0x35"); assert_eq!(probe.dword_candidates[9].value_i32, 10); - assert_eq!(probe.dword_candidates[10].label, "finance_neighborhood_word_11"); + assert_eq!( + probe.dword_candidates[10].label, + "finance_neighborhood_word_11" + ); assert_eq!(probe.dword_candidates[10].relative_offset_hex, "0x39"); assert_eq!(probe.dword_candidates[10].value_i32, 11); - assert_eq!(probe.dword_candidates[15].label, "finance_neighborhood_word_16"); + assert_eq!( + probe.dword_candidates[15].label, + "finance_neighborhood_word_16" + ); assert_eq!(probe.dword_candidates[15].relative_offset_hex, "0x4d"); assert_eq!(probe.dword_candidates[15].value_i32, 16); } @@ -15954,7 +15991,12 @@ mod tests { let slot_offset = record_offset + SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET + slot_index * SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE; - bytes[slot_offset..slot_offset + 4].copy_from_slice(&(500_000i32).to_le_bytes()); + let principal = if index == 0 && slot_index == 1 { + 650_000i32 + } else { + 500_000i32 + }; + bytes[slot_offset..slot_offset + 4].copy_from_slice(&principal.to_le_bytes()); bytes[slot_offset + 4..slot_offset + 8] .copy_from_slice(&(1894u32 + slot_index as u32).to_le_bytes()); bytes[slot_offset + 8..slot_offset + 12].copy_from_slice(&(0.10f32).to_le_bytes()); @@ -16055,7 +16097,7 @@ mod tests { assert_eq!(roster.entries.len(), 2); assert_eq!(roster.entries[0].company_id, 1); assert_eq!(roster.entries[0].linked_chairman_profile_id, Some(1)); - assert_eq!(roster.entries[0].debt, 1_000_000); + assert_eq!(roster.entries[0].debt, 1_150_000); assert_eq!(roster.entries[0].available_track_laying_capacity, Some(603)); assert_eq!(roster.entries[0].merger_cooldown_year, Some(1862)); let market_state = roster.entries[0] @@ -16063,6 +16105,7 @@ mod tests { .as_ref() .expect("company market state should load"); assert_eq!(market_state.outstanding_shares, 20_000); + assert_eq!(market_state.largest_live_bond_principal, Some(650_000)); assert_eq!(market_state.mutable_support_scalar_raw_u32, 0x3f800000); assert_eq!( market_state.young_company_support_scalar_raw_u32, @@ -16091,6 +16134,10 @@ mod tests { .market_state .as_ref() .expect("second company market state should load"); + assert_eq!( + second_market_state.largest_live_bond_principal, + Some(500_000) + ); assert_eq!(second_market_state.chairman_bonus_year, 0); assert_eq!(second_market_state.chairman_bonus_amount, 0); assert_eq!(second_market_state.last_dividend_year, 1850); diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 3d282e1..6b1c7dc 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; use crate::{ - runtime_company_annual_finance_state, runtime_company_unassigned_share_pool, CalendarPoint, - RuntimeState, + CalendarPoint, RuntimeState, runtime_company_annual_finance_state, + runtime_company_unassigned_share_pool, }; fn raw_u32_to_f32_text(raw: u32) -> String { @@ -51,6 +51,7 @@ pub struct RuntimeSummary { pub company_market_state_owner_count: usize, pub selected_company_outstanding_shares: Option, pub selected_company_bond_count: Option, + pub selected_company_largest_live_bond_principal: Option, pub selected_company_assigned_share_pool: Option, pub selected_company_unassigned_share_pool: Option, pub selected_company_cached_share_price: Option, @@ -261,6 +262,8 @@ impl RuntimeSummary { .map(|market_state| market_state.outstanding_shares), selected_company_bond_count: selected_company_market_state .map(|market_state| market_state.bond_count), + selected_company_largest_live_bond_principal: selected_company_market_state + .and_then(|market_state| market_state.largest_live_bond_principal), selected_company_assigned_share_pool: selected_company_annual_finance_state .as_ref() .map(|finance_state| finance_state.assigned_share_pool), @@ -1960,6 +1963,7 @@ mod tests { crate::RuntimeCompanyMarketState { outstanding_shares: 20_000, bond_count: 2, + largest_live_bond_principal: Some(500_000), mutable_support_scalar_raw_u32: 0x3f800000, young_company_support_scalar_raw_u32: 0x42340000, support_progress_word: 12, @@ -2028,6 +2032,10 @@ mod tests { assert_eq!(summary.company_market_state_owner_count, 1); assert_eq!(summary.selected_company_outstanding_shares, Some(20_000)); assert_eq!(summary.selected_company_bond_count, Some(2)); + assert_eq!( + summary.selected_company_largest_live_bond_principal, + Some(500_000) + ); assert_eq!(summary.selected_company_assigned_share_pool, Some(5_000)); assert_eq!(summary.selected_company_unassigned_share_pool, Some(15_000)); assert_eq!(summary.selected_company_cached_share_price, Some(40)); diff --git a/docs/README.md b/docs/README.md index e0aeb73..346f499 100644 --- a/docs/README.md +++ b/docs/README.md @@ -127,7 +127,8 @@ The highest-value next passes are now: feeds a shared company market reader for stock-capital, salary, bonus, and issue-calendar values, and now derives elapsed years since founding, last dividend, 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 + state for the stock-capital branch gate, and the grounded bond table now also contributes the + largest live bond principal into that same owner-state surface - 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 diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 0e550fa..746f1fe 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -218,7 +218,9 @@ helpers. That same owned annual-finance state now also derives elapsed years sin dividend, and last bankruptcy from the runtime calendar, which lines up directly with the grounded annual finance-policy gates in the atlas. Live bond-slot count now also flows through that same owned company market and annual-finance state, matching the stock-capital branch gate that needs -at least two live bonds. +at least two live bonds. The same grounded bond table now also contributes the largest 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. ## Why This Boundary