Rehost selected-year bucket scalar ladder

This commit is contained in:
Jan Petykiewicz 2026-04-18 06:59:06 -07:00
commit f9b3cf8571
10 changed files with 295 additions and 9 deletions

View file

@ -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

View file

@ -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
}
]
}

View file

@ -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] = &[

View file

@ -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,

View file

@ -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<String>,
#[serde(default)]
pub selected_year_bucket_scalar_raw_u32: Option<u32>,
#[serde(default)]
pub selected_year_bucket_scalar_value_f32_text: Option<String>,
#[serde(default)]
pub selected_year_gap_scalar_raw_u32: Option<u32>,
#[serde(default)]
pub selected_year_gap_scalar_value_f32_text: Option<String>,
@ -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<CheckedInSelectedYearBucketLadderEntry>,
}
#[derive(Debug, Clone, Deserialize)]
struct CheckedInSelectedYearBucketLadderEntry {
year: u32,
value: f32,
}
fn checked_in_selected_year_bucket_ladder() -> &'static [CheckedInSelectedYearBucketLadderEntry] {
static LADDER: OnceLock<Vec<CheckedInSelectedYearBucketLadderEntry>> = OnceLock::new();
LADDER
.get_or_init(|| {
serde_json::from_str::<CheckedInSelectedYearBucketLadderArtifact>(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<f32> {
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<f32> {
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())

View file

@ -80,6 +80,8 @@ pub struct RuntimeSummary {
pub world_restore_economic_tuning_lane_value_f32_text: Vec<String>,
pub world_restore_cached_available_locomotive_rating_raw_u32: Option<u32>,
pub world_restore_cached_available_locomotive_rating_value_f32_text: Option<String>,
pub world_restore_selected_year_bucket_scalar_raw_u32: Option<u32>,
pub world_restore_selected_year_bucket_scalar_value_f32_text: Option<String>,
pub world_restore_selected_year_gap_scalar_raw_u32: Option<u32>,
pub world_restore_selected_year_gap_scalar_value_f32_text: Option<String>,
pub world_restore_absolute_counter_restore_kind: Option<String>,
@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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("<I", data[i : i + 4])[0] for i in range(0, len(data), 4)]
def build_artifact(exe_bytes: bytes) -> 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())