From f9b3cf8571eb72d46649a2f048302684d082cc8a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 06:59:06 -0700 Subject: [PATCH] Rehost selected-year bucket scalar ladder --- README.md | 4 + .../rt3-1.06/selected-year-bucket-ladder.json | 94 ++++++++++++++++++ crates/rrt-model/src/lib.rs | 1 + crates/rrt-runtime/src/import.rs | 6 +- crates/rrt-runtime/src/runtime.rs | 99 +++++++++++++++++++ crates/rrt-runtime/src/summary.rs | 21 ++++ docs/README.md | 4 + docs/rehost-queue.md | 16 +-- docs/runtime-rehost-plan.md | 5 +- .../py/extract_selected_year_bucket_ladder.py | 54 ++++++++++ 10 files changed, 295 insertions(+), 9 deletions(-) create mode 100644 artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json create mode 100644 tools/py/extract_selected_year_bucket_ladder.py diff --git a/README.md b/README.md index 1812fed..8274753 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,10 @@ The same save-native world restore surface now also carries the grounded locomot and cached available-locomotive rating from the fixed world block, so the `All Steam/Diesel/Electric Locos Avail.` descriptor strip now writes through owner state instead of living only as ad hoc world flags. +The selected-year seam is now doing the same thing: the checked-in `0x00433bd0` year ladder now +drives a derived selected-year bucket scalar in runtime restore state, and the economic-tuning +mirror `[world+0x0bde]` now rebuilds from tuning lane `0` instead of freezing one stale load-time +word. Those bankruptcy branches now follow the grounded owner semantics too: they stamp the bankruptcy year and halve live bond principals in place instead of treating bankruptcy as a liquidation path. The same save-native live bond-slot surface now also carries per-slot maturity years all the way diff --git a/artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json b/artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json new file mode 100644 index 0000000..fe02742 --- /dev/null +++ b/artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json @@ -0,0 +1,94 @@ +{ + "source_kind": "rt3.exe-static-table", + "reader_family": "world_refresh_selected_year_bucket_scalar_band", + "table_virtual_address": "0x005f3980", + "pair_count": 21, + "terminal_scalar_virtual_address": "0x005f3a24", + "terminal_scalar_value": 123.0, + "entries": [ + { + "year": 1800, + "value": 10.0 + }, + { + "year": 1810, + "value": 15.0 + }, + { + "year": 1820, + "value": 20.0 + }, + { + "year": 1830, + "value": 25.0 + }, + { + "year": 1840, + "value": 38.0 + }, + { + "year": 1850, + "value": 45.0 + }, + { + "year": 1860, + "value": 50.0 + }, + { + "year": 1870, + "value": 55.0 + }, + { + "year": 1880, + "value": 60.0 + }, + { + "year": 1890, + "value": 65.0 + }, + { + "year": 1900, + "value": 70.0 + }, + { + "year": 1910, + "value": 75.0 + }, + { + "year": 1920, + "value": 80.0 + }, + { + "year": 1930, + "value": 65.0 + }, + { + "year": 1940, + "value": 90.0 + }, + { + "year": 1950, + "value": 95.0 + }, + { + "year": 1960, + "value": 95.0 + }, + { + "year": 1970, + "value": 95.0 + }, + { + "year": 1980, + "value": 100.0 + }, + { + "year": 1990, + "value": 110.0 + }, + { + "year": 2000, + "value": 123.0 + } + ] +} diff --git a/crates/rrt-model/src/lib.rs b/crates/rrt-model/src/lib.rs index 2641585..828ecf4 100644 --- a/crates/rrt-model/src/lib.rs +++ b/crates/rrt-model/src/lib.rs @@ -34,6 +34,7 @@ pub const REQUIRED_EXPORTS: &[&str] = &[ "artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json", "artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json", "artifacts/exports/rt3-1.06/economy-cargo-sources.json", + "artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json", ]; pub const REQUIRED_ATLAS_HEADINGS: &[&str] = &[ diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 43c5100..8f32304 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -982,6 +982,8 @@ fn project_save_slice_components( .cached_available_locomotive_rating_value_f32_text .clone() }), + selected_year_bucket_scalar_raw_u32: None, + selected_year_bucket_scalar_value_f32_text: None, selected_year_gap_scalar_raw_u32: None, selected_year_gap_scalar_value_f32_text: None, absolute_counter_restore_kind: Some( @@ -6448,7 +6450,7 @@ mod tests { ); assert_eq!( import.state.world_restore.economic_tuning_mirror_raw_u32, - Some(0x3f46d093) + Some(0x3f400000) ); assert_eq!( import @@ -6456,7 +6458,7 @@ mod tests { .world_restore .economic_tuning_mirror_value_f32_text .as_deref(), - Some("0.776620") + Some("0.750000") ); assert_eq!( import.state.world_restore.economic_tuning_lane_raw_u32, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 1601a5d..a12f076 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::sync::OnceLock; use serde::{Deserialize, Serialize}; @@ -1418,6 +1419,10 @@ pub struct RuntimeWorldRestoreState { #[serde(default)] pub cached_available_locomotive_rating_value_f32_text: Option, #[serde(default)] + pub selected_year_bucket_scalar_raw_u32: Option, + #[serde(default)] + pub selected_year_bucket_scalar_value_f32_text: Option, + #[serde(default)] pub selected_year_gap_scalar_raw_u32: Option, #[serde(default)] pub selected_year_gap_scalar_value_f32_text: Option, @@ -2396,6 +2401,11 @@ impl RuntimeState { .world_restore .all_electric_locomotives_available_raw_u8 .map(|raw| raw != 0); + if let Some(&lane_0_raw_u32) = self.world_restore.economic_tuning_lane_raw_u32.first() { + self.world_restore.economic_tuning_mirror_raw_u32 = Some(lane_0_raw_u32); + self.world_restore.economic_tuning_mirror_value_f32_text = + Some(format!("{:.6}", f32::from_bits(lane_0_raw_u32))); + } let year_word = self .world_restore .packed_year_word_raw_u16 @@ -2409,6 +2419,11 @@ impl RuntimeState { }) }) .unwrap_or(self.calendar.year); + if let Some(value) = runtime_world_selected_year_bucket_scalar_from_year_word(year_word) { + self.world_restore.selected_year_bucket_scalar_raw_u32 = Some(value.to_bits()); + self.world_restore + .selected_year_bucket_scalar_value_f32_text = Some(format!("{value:.6}")); + } if let Some(value) = runtime_world_selected_year_gap_scalar_from_year_word(year_word) { self.world_restore.selected_year_gap_scalar_raw_u32 = Some(value.to_bits()); self.world_restore.selected_year_gap_scalar_value_f32_text = @@ -2438,6 +2453,53 @@ impl RuntimeState { } } +#[derive(Debug, Clone, Deserialize)] +struct CheckedInSelectedYearBucketLadderArtifact { + entries: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +struct CheckedInSelectedYearBucketLadderEntry { + year: u32, + value: f32, +} + +fn checked_in_selected_year_bucket_ladder() -> &'static [CheckedInSelectedYearBucketLadderEntry] { + static LADDER: OnceLock> = OnceLock::new(); + LADDER + .get_or_init(|| { + serde_json::from_str::(include_str!( + "../../../artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json" + )) + .expect("checked-in selected-year bucket ladder should parse") + .entries + }) + .as_slice() +} + +pub fn runtime_world_selected_year_bucket_scalar_from_year_word(year_word: u32) -> Option { + let ladder = checked_in_selected_year_bucket_ladder(); + if ladder.is_empty() { + return None; + } + if year_word <= ladder[0].year { + return Some(ladder[0].value); + } + for window in ladder.windows(2) { + let start = &window[0]; + let end = &window[1]; + if year_word <= end.year { + if year_word <= start.year || end.year == start.year { + return Some(start.value); + } + let span = (end.year - start.year) as f32; + let progress = (year_word - start.year) as f32 / span; + return Some(start.value + (end.value - start.value) * progress); + } + } + ladder.last().map(|entry| entry.value) +} + pub fn runtime_world_selected_year_gap_scalar_from_year_word(year_word: u32) -> Option { let normalized = (year_word as f64 - 1850.0) / 150.0; if !normalized.is_finite() { @@ -5070,6 +5132,8 @@ mod tests { economic_tuning_mirror_value_f32_text: None, economic_tuning_lane_raw_u32: Vec::new(), economic_tuning_lane_value_f32_text: Vec::new(), + selected_year_bucket_scalar_raw_u32: None, + selected_year_bucket_scalar_value_f32_text: None, selected_year_gap_scalar_raw_u32: None, selected_year_gap_scalar_value_f32_text: None, absolute_counter_restore_kind: Some( @@ -8060,6 +8124,18 @@ mod tests { #[test] fn derives_selected_year_gap_scalar_from_year_word() { + assert_eq!( + runtime_world_selected_year_bucket_scalar_from_year_word(1830), + Some(25.0) + ); + assert_eq!( + runtime_world_selected_year_bucket_scalar_from_year_word(1835), + Some(31.5) + ); + assert_eq!( + runtime_world_selected_year_bucket_scalar_from_year_word(2000), + Some(123.0) + ); assert_eq!( runtime_world_selected_year_gap_scalar_from_year_word(1830), Some((1.0f32 / 3.0).clamp(1.0 / 3.0, 1.0)) @@ -8087,6 +8163,7 @@ mod tests { save_profile: RuntimeSaveProfileState::default(), world_restore: RuntimeWorldRestoreState { packed_year_word_raw_u16: Some(1900), + economic_tuning_lane_raw_u32: vec![0x3f400000], ..RuntimeWorldRestoreState::default() }, metadata: BTreeMap::new(), @@ -8125,6 +8202,28 @@ mod tests { state.refresh_derived_world_state(); + assert_eq!( + state.world_restore.economic_tuning_mirror_raw_u32, + Some(0x3f400000) + ); + assert_eq!( + state + .world_restore + .economic_tuning_mirror_value_f32_text + .as_deref(), + Some("0.750000") + ); + assert_eq!( + state.world_restore.selected_year_bucket_scalar_raw_u32, + Some(70.0f32.to_bits()) + ); + assert_eq!( + state + .world_restore + .selected_year_bucket_scalar_value_f32_text + .as_deref(), + Some("70.000000") + ); assert_eq!( state.world_restore.selected_year_gap_scalar_raw_u32, Some(((50.0f32 / 150.0).clamp(1.0 / 3.0, 1.0)).to_bits()) diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 8ae996e..36a20ef 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -80,6 +80,8 @@ pub struct RuntimeSummary { pub world_restore_economic_tuning_lane_value_f32_text: Vec, pub world_restore_cached_available_locomotive_rating_raw_u32: Option, pub world_restore_cached_available_locomotive_rating_value_f32_text: Option, + pub world_restore_selected_year_bucket_scalar_raw_u32: Option, + pub world_restore_selected_year_bucket_scalar_value_f32_text: Option, pub world_restore_selected_year_gap_scalar_raw_u32: Option, pub world_restore_selected_year_gap_scalar_value_f32_text: Option, pub world_restore_absolute_counter_restore_kind: Option, @@ -471,6 +473,13 @@ impl RuntimeSummary { .world_restore .cached_available_locomotive_rating_value_f32_text .clone(), + world_restore_selected_year_bucket_scalar_raw_u32: state + .world_restore + .selected_year_bucket_scalar_raw_u32, + world_restore_selected_year_bucket_scalar_value_f32_text: state + .world_restore + .selected_year_bucket_scalar_value_f32_text + .clone(), world_restore_selected_year_gap_scalar_raw_u32: state .world_restore .selected_year_gap_scalar_raw_u32, @@ -1807,6 +1816,8 @@ mod tests { all_electric_locomotives_available_enabled: Some(true), cached_available_locomotive_rating_raw_u32: Some(0x41a00000), cached_available_locomotive_rating_value_f32_text: Some("20.000000".to_string()), + selected_year_bucket_scalar_raw_u32: Some(25.0f32.to_bits()), + selected_year_bucket_scalar_value_f32_text: Some("25.000000".to_string()), selected_year_gap_scalar_raw_u32: Some(0x3eaaaaab), selected_year_gap_scalar_value_f32_text: Some("0.333333".to_string()), ..RuntimeWorldRestoreState::default() @@ -1961,6 +1972,16 @@ mod tests { .as_deref(), Some("20.000000") ); + assert_eq!( + summary.world_restore_selected_year_bucket_scalar_raw_u32, + Some(25.0f32.to_bits()) + ); + assert_eq!( + summary + .world_restore_selected_year_bucket_scalar_value_f32_text + .as_deref(), + Some("25.000000") + ); assert_eq!(summary.world_restore_economic_tuning_lane_count, 6); assert_eq!( summary.world_restore_economic_tuning_lane_value_f32_text, diff --git a/docs/README.md b/docs/README.md index ad2361f..06d6064 100644 --- a/docs/README.md +++ b/docs/README.md @@ -173,6 +173,10 @@ The highest-value next passes are now: and cached available-locomotive rating from the fixed world block, so the `All Steam/Diesel/Electric Locos Avail.` descriptor strip now writes through owner state instead of living only as mirrored world flags +- the selected-year seam now follows the same rule: the checked-in `0x00433bd0` year ladder now + drives a derived selected-year bucket scalar in runtime restore state, and the economic-tuning + mirror `[world+0x0bde]` now rebuilds from tuning lane `0` instead of freezing one stale + load-time word - 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/rehost-queue.md b/docs/rehost-queue.md index 5021300..52354fb 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -10,12 +10,12 @@ Working rule: ## Next - Rehost the next selected-year periodic-boundary world seam under - `simulation_service_periodic_boundary_work`, starting with the save-world economic tuning mirror - `[world+0x0bde]` and the directly adjacent selected-year bucket ladder rooted in the grounded - `0x00433bd0` reader family instead of another isolated scalar guess. -- Expand the selected-year world-owner surface beyond the stepped calendar, gap scalar, and - locomotive-policy lanes when the owning reader/rebuild family is grounded strongly enough to - avoid one-off leaf guesses. + `simulation_service_periodic_boundary_work`, extending the now-grounded selected-year bucket + scalar into the direct bucket trio `[world+0x65/+0x69/+0x6d]` and any safe follow-on companion + lanes rooted in `0x00433bd0`. +- Expand the selected-year world-owner surface beyond the stepped calendar, gap scalar, + bucket-scalar, mirror, and locomotive-policy lanes when the owning reader/rebuild family is + grounded strongly enough to avoid one-off leaf guesses. ## In Progress @@ -47,6 +47,10 @@ Working rule: - Save-native world locomotive policy owner state now flows through runtime restore state, summaries, and keyed world-flag execution for the grounded `All Steam/Diesel/Electric Locos Avail.` descriptor strip plus the cached available-locomotive rating. +- The selected-year bucket ladder rooted in `0x00433bd0` is now checked in as a static artifact, + and runtime restore state now derives both the selected-year bucket scalar and the + `[world+0x0bde]` economic-tuning mirror from owner-family inputs instead of preserving stale + load-time residue. - Company cash, confiscation, and major governance effects now write through owner state instead of drifting from market/cache readers. - Company credit rating, prime rate, book value per share, investor confidence, and management diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index e4bcfb7..deb1bed 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -220,7 +220,10 @@ scalar owner lane `[world+0x4ca2]`, so later selected-year periodic-boundary wor on runtime state instead of a frozen load-time scalar. That same save-native world restore surface now also carries the grounded locomotive-policy bytes and cached available-locomotive rating from the fixed world block, so the `All Steam/Diesel/Electric Locos Avail.` descriptor strip now writes -through owner state instead of living only as mirrored world flags. The same owned company annual-finance state +through owner state instead of living only as mirrored world flags. The selected-year seam now +follows the same owner rule: the checked-in `0x00433bd0` year ladder now drives a derived +selected-year bucket scalar in runtime restore state, and the economic-tuning mirror `[world+0x0bde]` +now rebuilds from tuning lane `0` instead of freezing one stale load-time word. The same owned company annual-finance state now also drives a shared company market reader seam for stock-capital, salary, bonus, and the full two-word current/prior issue-calendar tuples, which is a better base for shellless finance simulation than summary-only helpers. That same owned annual-finance state now also derives elapsed diff --git a/tools/py/extract_selected_year_bucket_ladder.py b/tools/py/extract_selected_year_bucket_ladder.py new file mode 100644 index 0000000..8a89e90 --- /dev/null +++ b/tools/py/extract_selected_year_bucket_ladder.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import struct +from pathlib import Path + +IMAGE_BASE = 0x400000 +PAIRED_YEAR_VALUE_TABLE_VA = 0x005F3980 +PAIR_COUNT = 21 +TERMINAL_SCALAR_VA = 0x005F3A24 + + +def read_u32_table(blob: bytes, va: int, count: int) -> list[int]: + offset = va - IMAGE_BASE + data = blob[offset : offset + count * 4] + if len(data) != count * 4: + raise ValueError(f"table at {va:#x} truncated") + return [struct.unpack(" dict[str, object]: + raw_pairs = read_u32_table(exe_bytes, PAIRED_YEAR_VALUE_TABLE_VA, PAIR_COUNT * 2) + terminal_scalar_raw = read_u32_table(exe_bytes, TERMINAL_SCALAR_VA, 1)[0] + entries = [] + for year, value in zip(raw_pairs[::2], raw_pairs[1::2]): + entries.append({"year": year, "value": float(value)}) + return { + "source_kind": "rt3.exe-static-table", + "reader_family": "world_refresh_selected_year_bucket_scalar_band", + "table_virtual_address": f"0x{PAIRED_YEAR_VALUE_TABLE_VA:08x}", + "pair_count": PAIR_COUNT, + "terminal_scalar_virtual_address": f"0x{TERMINAL_SCALAR_VA:08x}", + "terminal_scalar_value": float(terminal_scalar_raw), + "entries": entries, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Extract the selected-year bucket ladder used by RT3 world year refresh." + ) + parser.add_argument("exe", type=Path, help="Path to RT3.exe") + parser.add_argument("output", type=Path, help="Output JSON path") + args = parser.parse_args() + + artifact = build_artifact(args.exe.read_bytes()) + args.output.write_text(json.dumps(artifact, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())