Rehost selected-year bucket scalar ladder
This commit is contained in:
parent
95215d836e
commit
f9b3cf8571
10 changed files with 295 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
94
artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json
Normal file
94
artifacts/exports/rt3-1.06/selected-year-bucket-ladder.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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] = &[
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
54
tools/py/extract_selected_year_bucket_ladder.py
Normal file
54
tools/py/extract_selected_year_bucket_ladder.py
Normal 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue