diff --git a/README.md b/README.md index df6e895..4b53c17 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ headlessly as a pure runtime reader over owned annual-finance state, support-adj and current world finance policy rather than as a notes-only atlas fragment. The later deep- distress bankruptcy fallback is now rehosted on that same owner surface too, using the save-native cash reader seam plus the first three trailing net-profit years instead of another ad hoc probe. +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 +pure reader over owned save-native state instead of a guessed finance-side approximation. 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-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index aac036e..106ef67 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -108,6 +108,7 @@ struct SaveSliceProjection { has_chairman_selection_override: bool, selected_chairman_profile_id: Option, chairman_issue_opinion_terms_raw_i32: BTreeMap>, + chairman_personality_raw_u8: BTreeMap, candidate_availability: BTreeMap, named_locomotive_availability: BTreeMap, locomotive_catalog: Option>, @@ -318,6 +319,7 @@ pub fn project_save_slice_to_runtime_state_import( .world_issue_opinion_base_terms_raw_i32, company_market_state: projection.company_market_state, chairman_issue_opinion_terms_raw_i32: projection.chairman_issue_opinion_terms_raw_i32, + chairman_personality_raw_u8: projection.chairman_personality_raw_u8, ..RuntimeServiceState::default() }, }; @@ -448,6 +450,11 @@ pub fn project_save_slice_overlay_to_runtime_state_import( .chairman_issue_opinion_terms_raw_i32 .clone() }, + chairman_personality_raw_u8: if projection.has_chairman_projection { + projection.chairman_personality_raw_u8 + } else { + base_state.service_state.chairman_personality_raw_u8.clone() + }, ..base_state.service_state.clone() }, }; @@ -867,6 +874,10 @@ fn project_save_slice_components( .world_finance_neighborhood_state .as_ref() .map(|state| state.dividend_policy_raw_u8), + building_density_growth_setting_raw_u32: save_slice + .world_finance_neighborhood_state + .as_ref() + .map(|state| state.building_density_growth_setting_raw_u32), stock_issue_and_buyback_allowed: save_slice .world_finance_neighborhood_state .as_ref() @@ -1227,6 +1238,7 @@ fn project_save_slice_components( has_chairman_selection_override, selected_chairman_profile_id, chairman_issue_opinion_terms_raw_i32, + chairman_personality_raw_u8, ) = if let Some(table) = &save_slice.chairman_profile_table { metadata.insert( "save_slice.chairman_profile_table_source_kind".to_string(), @@ -1253,6 +1265,7 @@ fn project_save_slice_components( table.selected_chairman_profile_id.is_some(), table.selected_chairman_profile_id, BTreeMap::new(), + BTreeMap::new(), ) } else { ( @@ -1279,10 +1292,26 @@ fn project_save_slice_components( .iter() .map(|entry| (entry.profile_id, entry.issue_opinion_terms_raw_i32.clone())) .collect::>(), + table + .entries + .iter() + .filter_map(|entry| { + entry + .personality_byte_0x291 + .map(|value| (entry.profile_id, value)) + }) + .collect::>(), ) } } else { - (Vec::new(), false, false, None, BTreeMap::new()) + ( + Vec::new(), + false, + false, + None, + BTreeMap::new(), + BTreeMap::new(), + ) }; let named_locomotive_cost = BTreeMap::new(); @@ -1374,6 +1403,7 @@ fn project_save_slice_components( has_chairman_selection_override, selected_chairman_profile_id, chairman_issue_opinion_terms_raw_i32, + chairman_personality_raw_u8, candidate_availability, named_locomotive_availability, locomotive_catalog, @@ -5326,6 +5356,7 @@ mod tests { holdings_value_total: 700, net_worth_total: 1200, purchasing_power_total: 1500, + personality_byte_0x291: Some(12), issue_opinion_terms_raw_i32: Vec::new(), }, crate::SmpLoadedChairmanProfileEntry { @@ -5338,6 +5369,7 @@ mod tests { holdings_value_total: 600, net_worth_total: 900, purchasing_power_total: 1100, + personality_byte_0x291: Some(20), issue_opinion_terms_raw_i32: Vec::new(), }, ], @@ -6019,6 +6051,8 @@ mod tests { bankruptcy_policy_raw_hex: "0x00".to_string(), dividend_policy_raw_u8: 1, dividend_policy_raw_hex: "0x01".to_string(), + building_density_growth_setting_raw_u32: 1, + building_density_growth_setting_raw_hex: "0x00000001".to_string(), labels: vec![ "current_calendar_tuple_word".to_string(), "current_calendar_tuple_word_2".to_string(), @@ -13956,6 +13990,7 @@ mod tests { world_issue_opinion_base_terms_raw_i32: Vec::new(), company_market_state: BTreeMap::new(), chairman_issue_opinion_terms_raw_i32: BTreeMap::new(), + chairman_personality_raw_u8: BTreeMap::new(), }, }; let save_slice = SmpLoadedSaveSlice { diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 74df880..7c2ee03 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -53,12 +53,13 @@ pub use runtime::{ RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState, - RuntimeCompanyAnnualFinanceState, RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, - RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, - RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, - RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, - RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, - RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, + RuntimeCompanyAnnualFinanceState, RuntimeCompanyAnnualStockRepurchaseState, + 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, @@ -67,17 +68,18 @@ pub use runtime::{ RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldIssueState, RuntimeWorldRestoreState, runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_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_annual_finance_state, runtime_company_annual_stock_repurchase_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, + runtime_world_bond_issue_and_repayment_allowed, runtime_world_building_density_growth_setting, + 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, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 92ed875..760f977 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -227,6 +227,37 @@ pub struct RuntimeCompanyAnnualDeepDistressState { pub eligible_for_bankruptcy_fallback: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeCompanyAnnualStockRepurchaseState { + pub company_id: u32, + #[serde(default)] + pub annual_mode_active: Option, + #[serde(default)] + pub stock_issue_and_buyback_allowed: Option, + pub city_connection_latch: bool, + #[serde(default)] + pub building_density_growth_setting: Option, + #[serde(default)] + pub linked_chairman_profile_id: Option, + #[serde(default)] + pub linked_chairman_personality_raw_u8: Option, + #[serde(default)] + pub repurchase_batch_size: Option, + #[serde(default)] + pub repurchase_factor_basis_points: Option, + #[serde(default)] + pub current_cash: Option, + #[serde(default)] + pub stock_value_gate_cash_floor: Option, + #[serde(default)] + pub support_adjusted_share_price_scalar: Option, + #[serde(default)] + pub affordability_cash_floor: Option, + #[serde(default)] + pub unassigned_share_pool: Option, + pub eligible_for_single_batch_repurchase: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeTrackPieceCounts { #[serde(default)] @@ -1044,6 +1075,8 @@ pub struct RuntimeServiceState { pub company_market_state: BTreeMap, #[serde(default)] pub chairman_issue_opinion_terms_raw_i32: BTreeMap>, + #[serde(default)] + pub chairman_personality_raw_u8: BTreeMap, } #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] @@ -1159,6 +1192,8 @@ pub struct RuntimeWorldRestoreState { #[serde(default)] pub dividend_policy_raw_u8: Option, #[serde(default)] + pub building_density_growth_setting_raw_u32: Option, + #[serde(default)] pub stock_issue_and_buyback_allowed: Option, #[serde(default)] pub bond_issue_and_repayment_allowed: Option, @@ -1953,6 +1988,14 @@ impl RuntimeState { )); } } + for chairman_profile_id in self.service_state.chairman_personality_raw_u8.keys() { + if !seen_chairman_profile_ids.contains(chairman_profile_id) { + return Err(format!( + "service_state.chairman_personality_raw_u8 references unknown chairman_profile_id {}", + chairman_profile_id + )); + } + } for (player_id, vars) in &self.player_runtime_variables { if !seen_player_ids.contains(player_id) { return Err(format!( @@ -2804,6 +2847,95 @@ pub fn runtime_world_dividend_adjustment_allowed(state: &RuntimeState) -> Option Some(state.world_restore.dividend_policy_raw_u8? == 0) } +pub fn runtime_world_building_density_growth_setting(state: &RuntimeState) -> Option { + state.world_restore.building_density_growth_setting_raw_u32 +} + +fn runtime_chairman_stock_repurchase_factor_f64( + state: &RuntimeState, + chairman_profile_id: Option, +) -> Option { + let personality_byte = chairman_profile_id + .and_then(|profile_id| { + state + .service_state + .chairman_personality_raw_u8 + .get(&profile_id) + }) + .copied(); + let mut factor = personality_byte + .map(|byte| (f64::from(byte) * 39.0 + 300.0) / 400.0) + .unwrap_or(1.0); + if runtime_world_building_density_growth_setting(state) == Some(1) { + factor *= 1.6; + } + Some(factor) +} + +pub fn runtime_company_annual_stock_repurchase_state( + state: &RuntimeState, + company_id: u32, +) -> Option { + let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let company = state + .companies + .iter() + .find(|company| company.company_id == company_id)?; + let current_cash = runtime_company_control_transfer_stat_value_f64( + state, + company_id, + RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH, + ) + .and_then(runtime_round_f64_to_i64); + let support_adjusted_share_price_scalar = + runtime_company_support_adjusted_share_price_scalar_f64(state, company_id); + let repurchase_factor = + runtime_chairman_stock_repurchase_factor_f64(state, company.linked_chairman_profile_id)?; + let repurchase_factor_basis_points = runtime_round_f64_to_i64(repurchase_factor * 100.0); + let stock_value_gate_cash_floor = runtime_round_f64_to_i64(repurchase_factor * 800_000.0); + let affordability_cash_floor = support_adjusted_share_price_scalar + .and_then(|value| runtime_round_f64_to_i64(value * repurchase_factor * 1_000.0 * 1.2)); + let support_adjusted_share_price_scalar = + support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64); + let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id); + let eligible_for_single_batch_repurchase = runtime_world_annual_finance_mode_active(state) + == Some(true) + && runtime_world_stock_issue_and_buyback_allowed(state) == Some(true) + && annual_finance_state.city_connection_latch + && current_cash + .zip(stock_value_gate_cash_floor) + .is_some_and(|(value, floor)| value >= floor) + && current_cash + .zip(affordability_cash_floor) + .is_some_and(|(value, floor)| value >= floor) + && unassigned_share_pool.is_some_and(|value| value >= 1_000); + Some(RuntimeCompanyAnnualStockRepurchaseState { + company_id, + annual_mode_active: runtime_world_annual_finance_mode_active(state), + stock_issue_and_buyback_allowed: runtime_world_stock_issue_and_buyback_allowed(state), + city_connection_latch: annual_finance_state.city_connection_latch, + building_density_growth_setting: runtime_world_building_density_growth_setting(state), + linked_chairman_profile_id: company.linked_chairman_profile_id, + linked_chairman_personality_raw_u8: company + .linked_chairman_profile_id + .and_then(|profile_id| { + state + .service_state + .chairman_personality_raw_u8 + .get(&profile_id) + }) + .copied(), + repurchase_batch_size: Some(1_000), + repurchase_factor_basis_points, + current_cash, + stock_value_gate_cash_floor, + support_adjusted_share_price_scalar, + affordability_cash_floor, + unassigned_share_pool, + eligible_for_single_batch_repurchase, + }) +} + pub fn runtime_company_annual_creditor_pressure_state( state: &RuntimeState, company_id: u32, @@ -3767,6 +3899,7 @@ mod tests { bond_issue_and_repayment_policy_raw_u8: None, bankruptcy_policy_raw_u8: None, dividend_policy_raw_u8: None, + building_density_growth_setting_raw_u32: None, stock_issue_and_buyback_allowed: None, bond_issue_and_repayment_allowed: None, bankruptcy_allowed: None, @@ -7113,6 +7246,132 @@ mod tests { assert!(pressure_state.eligible_for_bankruptcy_fallback); } + #[test] + fn derives_annual_stock_repurchase_state_from_rehosted_owner_state() { + let mut year_stat_family_qword_bits = vec![ + 0u64; + ((RUNTIME_COMPANY_STAT_SLOT_COUNT + 2) * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) + as usize + ]; + let write_current_value = |bits: &mut Vec, slot_id: u32, value: f64| { + let index = (slot_id * RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 1_600_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 { + partial_year_progress_raw_u8: Some(0x0c), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(1), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 12, + 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: Some(3), + 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![RuntimeChairmanProfile { + profile_id: 3, + name: "Jay".to_string(), + active: true, + current_cash: 200, + linked_company_id: Some(12), + company_holdings: BTreeMap::from([(12, 14_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + 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([( + 12, + RuntimeCompanyMarketState { + outstanding_shares: 20_000, + cached_share_price_raw_u32: 20.0f32.to_bits(), + founding_year: 1835, + city_connection_latch: true, + year_stat_family_qword_bits, + ..RuntimeCompanyMarketState::default() + }, + )]), + chairman_personality_raw_u8: BTreeMap::from([(3, 20)]), + ..RuntimeServiceState::default() + }, + }; + + let repurchase_state = runtime_company_annual_stock_repurchase_state(&state, 12) + .expect("stock repurchase state"); + assert_eq!(repurchase_state.building_density_growth_setting, Some(1)); + assert_eq!( + repurchase_state.linked_chairman_personality_raw_u8, + Some(20) + ); + assert_eq!(repurchase_state.repurchase_batch_size, Some(1_000)); + assert_eq!(repurchase_state.repurchase_factor_basis_points, Some(432)); + assert_eq!(repurchase_state.current_cash, Some(1_600_000)); + assert_eq!( + repurchase_state.stock_value_gate_cash_floor, + Some(3_456_000) + ); + assert_eq!( + repurchase_state.support_adjusted_share_price_scalar, + Some(20) + ); + assert_eq!(repurchase_state.affordability_cash_floor, Some(103_680)); + assert_eq!(repurchase_state.unassigned_share_pool, Some(5_500)); + assert!(!repurchase_state.eligible_for_single_batch_repurchase); + } + #[test] fn reads_company_market_metrics_from_annual_finance_reader() { let current_issue_calendar_word = 0x0101_0726; diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 8affe9e..4b8ef6d 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -137,6 +137,7 @@ 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_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET: usize = 0x4c78; 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]; @@ -1584,6 +1585,7 @@ pub struct SmpSaveWorldFinanceNeighborhoodProbe { pub bankruptcy_policy_raw_hex: String, pub dividend_policy_raw_u8: u8, pub dividend_policy_raw_hex: String, + pub building_density_growth_setting_lane: SmpSaveDwordCandidate, pub dword_candidates: Vec, pub evidence: Vec, } @@ -2276,6 +2278,8 @@ pub struct SmpLoadedWorldFinanceNeighborhoodState { pub bankruptcy_policy_raw_hex: String, pub dividend_policy_raw_u8: u8, pub dividend_policy_raw_hex: String, + pub building_density_growth_setting_raw_u32: u32, + pub building_density_growth_setting_raw_hex: String, pub labels: Vec, pub relative_offsets: Vec, pub relative_offset_hex: Vec, @@ -2345,6 +2349,8 @@ pub struct SmpLoadedChairmanProfileEntry { #[serde(default)] pub purchasing_power_total: i64, #[serde(default)] + pub personality_byte_0x291: Option, + #[serde(default)] pub issue_opinion_terms_raw_i32: Vec, } @@ -2457,6 +2463,8 @@ pub struct SmpSaveChairmanRecordAnalysisEntry { pub derived_net_worth_share_price_total: Option, #[serde(default)] pub derived_cached_purchasing_power_total: Option, + pub personality_byte_0x291: u8, + pub personality_byte_0x291_hex: String, #[serde(default)] pub cached_scalar_candidates: Vec, } @@ -3255,6 +3263,10 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( &bytes, record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, )?; + let personality_byte_0x291 = read_u8_at( + &bytes, + record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET, + )?; let mut holdings_by_company = BTreeMap::new(); for company_id in 1..=company_id_bound { let slot_offset = record_offset @@ -3293,6 +3305,8 @@ pub fn inspect_save_company_and_chairman_analysis_bytes( derived_holdings_share_price_total, derived_net_worth_share_price_total, derived_cached_purchasing_power_total, + personality_byte_0x291, + personality_byte_0x291_hex: format!("0x{personality_byte_0x291:02x}"), cached_scalar_candidates, }); } @@ -3528,6 +3542,11 @@ fn derive_loaded_world_finance_neighborhood_state_from_probe( 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(), + building_density_growth_setting_raw_u32: probe.building_density_growth_setting_lane.raw_u32, + building_density_growth_setting_raw_hex: probe + .building_density_growth_setting_lane + .raw_u32_hex + .clone(), labels: probe .dword_candidates .iter() @@ -3688,6 +3707,7 @@ const SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET: usize = 0x15d; const SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET: usize = 0x1dd; const SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET: usize = 0x1e9; const SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET: usize = 0x1f1; +const SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET: usize = 0x291; const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x35b; const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT: usize = RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT; @@ -4364,6 +4384,10 @@ fn parse_save_chairman_profile_table_probe( bytes, record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, )?; + let personality_byte_0x291 = read_u8_at( + bytes, + record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET, + )?; let cache_0 = round_f64_to_i64(read_f64_at( bytes, record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET, @@ -4409,6 +4433,7 @@ fn parse_save_chairman_profile_table_probe( holdings_value_total, net_worth_total, purchasing_power_total, + personality_byte_0x291: Some(personality_byte_0x291), issue_opinion_terms_raw_i32, }); } @@ -9144,6 +9169,12 @@ fn parse_save_world_finance_neighborhood_probe( bytes, payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET, )?; + let building_density_growth_setting_lane = build_save_dword_candidate( + bytes, + payload_offset, + "building_density_growth_setting", + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET, + )?; let dword_candidates = build_save_world_finance_neighborhood_candidates(bytes, payload_offset)?; @@ -9171,6 +9202,7 @@ fn parse_save_world_finance_neighborhood_probe( 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}"), + building_density_growth_setting_lane, dword_candidates, evidence: vec![ format!( @@ -9192,6 +9224,10 @@ fn parse_save_world_finance_neighborhood_probe( RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET ), + format!( + "payload +0x{:x} carries the fixed-world building-density growth setting mirrored from `[world+0x4c7c]`, which the annual repurchase and dividend policy helpers both read directly", + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_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(), ], }); @@ -15931,6 +15967,9 @@ mod tests { 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; + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET + ..payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET + 4] + .copy_from_slice(&2u32.to_le_bytes()); 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()); @@ -15968,6 +16007,13 @@ mod tests { 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.building_density_growth_setting_lane.raw_u32, 2); + assert_eq!( + probe + .building_density_growth_setting_lane + .relative_offset_hex, + "0x4c78" + ); assert_eq!(probe.current_calendar_tuple_word_lane.value_i32, 1); assert_eq!( probe.current_calendar_tuple_word_2_lane.relative_offset_hex, @@ -16773,6 +16819,8 @@ mod tests { bytes[record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET ..record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET + 4] .copy_from_slice(&linked.to_le_bytes()); + bytes[record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET] = + (index as u8) + 10; bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET ..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET + 8] .copy_from_slice(&cache0.to_le_bytes()); @@ -16857,10 +16905,12 @@ mod tests { assert_eq!(table.entries[0].current_cash, -107644); assert_eq!(table.entries[0].holdings_value_total, 252508); assert_eq!(table.entries[0].purchasing_power_total, 144864); + assert_eq!(table.entries[0].personality_byte_0x291, Some(10)); assert_eq!(table.entries[1].profile_id, 2); assert_eq!(table.entries[1].company_holdings.get(&2), Some(&9000)); assert_eq!(table.entries[1].holdings_value_total, 822000); assert_eq!(table.entries[1].purchasing_power_total, 1_009_282); + assert_eq!(table.entries[1].personality_byte_0x291, Some(11)); } #[test] diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index d0b0b03..fde1a26 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{ CalendarPoint, RuntimeState, runtime_company_annual_creditor_pressure_state, runtime_company_annual_deep_distress_state, runtime_company_annual_finance_state, - runtime_company_unassigned_share_pool, + runtime_company_annual_stock_repurchase_state, runtime_company_unassigned_share_pool, }; fn raw_u32_to_f32_text(raw: u32) -> String { @@ -55,6 +55,7 @@ pub struct RuntimeSummary { pub world_restore_bond_issue_and_repayment_policy_raw_u8: Option, pub world_restore_bankruptcy_policy_raw_u8: Option, pub world_restore_dividend_policy_raw_u8: Option, + pub world_restore_building_density_growth_setting_raw_u32: Option, pub world_restore_stock_issue_and_buyback_allowed: Option, pub world_restore_bond_issue_and_repayment_allowed: Option, pub world_restore_bankruptcy_allowed: Option, @@ -111,6 +112,18 @@ pub struct RuntimeSummary { pub selected_company_deep_distress_cash_floor: Option, pub selected_company_deep_distress_net_profit_floor: Option, pub selected_company_deep_distress_eligible_for_bankruptcy_fallback: Option, + pub selected_company_stock_repurchase_city_connection_latch: Option, + pub selected_company_stock_repurchase_building_density_growth_setting: Option, + pub selected_company_stock_repurchase_linked_chairman_profile_id: Option, + pub selected_company_stock_repurchase_linked_chairman_personality_raw_u8: Option, + pub selected_company_stock_repurchase_batch_size: Option, + pub selected_company_stock_repurchase_factor_basis_points: Option, + pub selected_company_stock_repurchase_current_cash: Option, + pub selected_company_stock_repurchase_stock_value_gate_cash_floor: Option, + pub selected_company_stock_repurchase_support_adjusted_share_price_scalar: Option, + pub selected_company_stock_repurchase_affordability_cash_floor: Option, + pub selected_company_stock_repurchase_unassigned_share_pool: Option, + pub selected_company_stock_repurchase_eligible_for_single_batch: Option, pub player_count: usize, pub chairman_profile_count: usize, pub active_chairman_profile_count: usize, @@ -207,6 +220,10 @@ impl RuntimeSummary { let selected_company_deep_distress_state = state .selected_company_id .and_then(|company_id| runtime_company_annual_deep_distress_state(state, company_id)); + let selected_company_stock_repurchase_state = + state.selected_company_id.and_then(|company_id| { + runtime_company_annual_stock_repurchase_state(state, company_id) + }); Self { calendar: state.calendar, calendar_projection_source: state.metadata.get("save_slice.calendar_source").cloned(), @@ -294,6 +311,9 @@ impl RuntimeSummary { .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_building_density_growth_setting_raw_u32: state + .world_restore + .building_density_growth_setting_raw_u32, world_restore_stock_issue_and_buyback_allowed: state .world_restore .stock_issue_and_buyback_allowed, @@ -480,6 +500,56 @@ impl RuntimeSummary { selected_company_deep_distress_state .as_ref() .map(|pressure_state| pressure_state.eligible_for_bankruptcy_fallback), + selected_company_stock_repurchase_city_connection_latch: + selected_company_stock_repurchase_state + .as_ref() + .map(|repurchase_state| repurchase_state.city_connection_latch), + selected_company_stock_repurchase_building_density_growth_setting: + selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.building_density_growth_setting), + selected_company_stock_repurchase_linked_chairman_profile_id: + selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.linked_chairman_profile_id), + selected_company_stock_repurchase_linked_chairman_personality_raw_u8: + selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| { + repurchase_state.linked_chairman_personality_raw_u8 + }), + selected_company_stock_repurchase_batch_size: selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.repurchase_batch_size), + selected_company_stock_repurchase_factor_basis_points: + selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.repurchase_factor_basis_points), + selected_company_stock_repurchase_current_cash: selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.current_cash), + selected_company_stock_repurchase_stock_value_gate_cash_floor: + selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.stock_value_gate_cash_floor), + selected_company_stock_repurchase_support_adjusted_share_price_scalar: + selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| { + repurchase_state.support_adjusted_share_price_scalar + }), + selected_company_stock_repurchase_affordability_cash_floor: + selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.affordability_cash_floor), + selected_company_stock_repurchase_unassigned_share_pool: + selected_company_stock_repurchase_state + .as_ref() + .and_then(|repurchase_state| repurchase_state.unassigned_share_pool), + selected_company_stock_repurchase_eligible_for_single_batch: + selected_company_stock_repurchase_state + .as_ref() + .map(|repurchase_state| repurchase_state.eligible_for_single_batch_repurchase), player_count: state.players.len(), chairman_profile_count: state.chairman_profiles.len(), active_chairman_profile_count: state @@ -2590,4 +2660,155 @@ mod tests { Some(true) ); } + + #[test] + fn summarizes_selected_company_stock_repurchase_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, slot_id: u32, value: f64| { + let index = (slot_id * crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN) as usize; + bits[index] = value.to_bits(); + }; + write_current_value(&mut year_stat_family_qword_bits, 0x0d, 1_600_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 { + partial_year_progress_raw_u8: Some(0x0c), + stock_issue_and_buyback_policy_raw_u8: Some(0), + stock_issue_and_buyback_allowed: Some(true), + building_density_growth_setting_raw_u32: Some(1), + ..RuntimeWorldRestoreState::default() + }, + metadata: BTreeMap::new(), + companies: vec![RuntimeCompany { + company_id: 12, + 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: Some(3), + 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(12), + players: Vec::new(), + selected_player_id: None, + chairman_profiles: vec![crate::RuntimeChairmanProfile { + profile_id: 3, + name: "Jay".to_string(), + active: true, + current_cash: 200, + linked_company_id: Some(12), + company_holdings: BTreeMap::from([(12, 14_500)]), + holdings_value_total: 0, + net_worth_total: 0, + purchasing_power_total: 0, + }], + 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: Vec::new().into_iter().collect(), + 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([( + 12, + crate::RuntimeCompanyMarketState { + outstanding_shares: 20_000, + cached_share_price_raw_u32: 20.0f32.to_bits(), + founding_year: 1835, + city_connection_latch: true, + year_stat_family_qword_bits, + ..crate::RuntimeCompanyMarketState::default() + }, + )]), + chairman_personality_raw_u8: BTreeMap::from([(3, 20)]), + ..RuntimeServiceState::default() + }, + }; + + let summary = RuntimeSummary::from_state(&state); + assert_eq!( + summary.world_restore_building_density_growth_setting_raw_u32, + Some(1) + ); + assert_eq!( + summary.selected_company_stock_repurchase_building_density_growth_setting, + Some(1) + ); + assert_eq!( + summary.selected_company_stock_repurchase_linked_chairman_personality_raw_u8, + Some(20) + ); + assert_eq!( + summary.selected_company_stock_repurchase_batch_size, + Some(1_000) + ); + assert_eq!( + summary.selected_company_stock_repurchase_factor_basis_points, + Some(432) + ); + assert_eq!( + summary.selected_company_stock_repurchase_current_cash, + Some(1_600_000) + ); + assert_eq!( + summary.selected_company_stock_repurchase_stock_value_gate_cash_floor, + Some(3_456_000) + ); + assert_eq!( + summary.selected_company_stock_repurchase_support_adjusted_share_price_scalar, + Some(20) + ); + assert_eq!( + summary.selected_company_stock_repurchase_affordability_cash_floor, + Some(103_680) + ); + assert_eq!( + summary.selected_company_stock_repurchase_unassigned_share_pool, + Some(5_500) + ); + assert_eq!( + summary.selected_company_stock_repurchase_eligible_for_single_batch, + Some(false) + ); + } } diff --git a/docs/README.md b/docs/README.md index 174b025..4d80ffd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -138,7 +138,9 @@ The highest-value next passes are now: 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 later deep-distress bankruptcy fallback now runs on that same save-native cash and trailing- - net-profit surface too + net-profit surface too; the same owner 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 headlessly as another pure reader - 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 30cc96a..c8fd521 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -231,6 +231,9 @@ bankruptcy branch now runs as a pure runtime reader over owned annual-finance st adjusted share price, and those policy bytes rather than staying in atlas prose only. The later deep-distress bankruptcy fallback now rides the same owner-state seam too, using the save-native cash reader plus the first three trailing net-profit years instead of a parallel raw-offset guess. +That same seam now also carries the fixed-world building-density growth setting plus the linked +chairman personality byte, which is enough to rehost the annual stock-repurchase gate on owned +save/runtime state instead of another threshold-only note. ## Why This Boundary