diff --git a/README.md b/README.md
index 463479a..f5d7388 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,9 @@ and it now also carries the first grounded stat-band root windows at `[company+0
build on owned state instead of another round of single-field save-offset guesses. The first
runtime-side `0x2329` stat-family reader seam is now rehosted too for the currently grounded slots
`0x0d` (`current_cash`) and `0x1d` (`book_value_per_share`), so later annual-finance logic can
-extend one shared reader family instead of hard-coding more direct field accesses. A checked-in
+extend one shared reader family instead of hard-coding more direct field accesses. Those saved
+stat-band windows are now widened to 16 dwords per root in save-slice/runtime state so later
+year-series finance closure has enough owned raw state to attach to. A checked-in
The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we
should prefer rehosting the owning source state or the real reader/setter family rather than
guessing one more derived leaf field from nearby offsets. A checked-in
diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs
index 057241a..7c59646 100644
--- a/crates/rrt-runtime/src/smp.rs
+++ b/crates/rrt-runtime/src/smp.rs
@@ -3070,27 +3070,27 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
})
.collect::>>()?;
- let stat_band_root_0cfb_candidates =
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_CANDIDATE_FIELDS
- .iter()
- .map(|(label, relative_offset)| {
- build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
- })
- .collect:: >>()?;
- let stat_band_root_0d7f_candidates =
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_CANDIDATE_FIELDS
- .iter()
- .map(|(label, relative_offset)| {
- build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
- })
- .collect:: >>()?;
- let stat_band_root_1c47_candidates =
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_CANDIDATE_FIELDS
- .iter()
- .map(|(label, relative_offset)| {
- build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
- })
- .collect:: >>()?;
+ let stat_band_root_0cfb_candidates = build_save_company_stat_band_candidates(
+ &bytes,
+ record_offset,
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET,
+ "stat_band_0cfb",
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
+ )?;
+ let stat_band_root_0d7f_candidates = build_save_company_stat_band_candidates(
+ &bytes,
+ record_offset,
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET,
+ "stat_band_0d7f",
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
+ )?;
+ let stat_band_root_1c47_candidates = build_save_company_stat_band_candidates(
+ &bytes,
+ record_offset,
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET,
+ "stat_band_1c47",
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
+ )?;
entries.push(SmpSaveCompanyRecordAnalysisEntry {
company_id,
name,
@@ -3498,6 +3498,7 @@ const SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET: usize = 0x7680;
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET: usize = 0x0cfb;
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET: usize = 0x0d7f;
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET: usize = 0x1c47;
+const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS: usize = 16;
const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 7] = [
("mutable_support_scalar", 0x4f),
("young_company_support_scalar", 0x57),
@@ -3527,108 +3528,6 @@ const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [(&str, usize); 6] = [
("post_capacity_word_5", 0x7694),
("post_capacity_word_6", 0x7698),
];
-const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_CANDIDATE_FIELDS: [(&str, usize); 8] = [
- (
- "stat_band_0cfb_word_1",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET,
- ),
- (
- "stat_band_0cfb_word_2",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 4,
- ),
- (
- "stat_band_0cfb_word_3",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 8,
- ),
- (
- "stat_band_0cfb_word_4",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 12,
- ),
- (
- "stat_band_0cfb_word_5",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 16,
- ),
- (
- "stat_band_0cfb_word_6",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 20,
- ),
- (
- "stat_band_0cfb_word_7",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 24,
- ),
- (
- "stat_band_0cfb_word_8",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 28,
- ),
-];
-const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_CANDIDATE_FIELDS: [(&str, usize); 8] = [
- (
- "stat_band_0d7f_word_1",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET,
- ),
- (
- "stat_band_0d7f_word_2",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET + 4,
- ),
- (
- "stat_band_0d7f_word_3",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET + 8,
- ),
- (
- "stat_band_0d7f_word_4",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET + 12,
- ),
- (
- "stat_band_0d7f_word_5",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET + 16,
- ),
- (
- "stat_band_0d7f_word_6",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET + 20,
- ),
- (
- "stat_band_0d7f_word_7",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET + 24,
- ),
- (
- "stat_band_0d7f_word_8",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET + 28,
- ),
-];
-const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_CANDIDATE_FIELDS: [(&str, usize); 8] = [
- (
- "stat_band_1c47_word_1",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET,
- ),
- (
- "stat_band_1c47_word_2",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET + 4,
- ),
- (
- "stat_band_1c47_word_3",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET + 8,
- ),
- (
- "stat_band_1c47_word_4",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET + 12,
- ),
- (
- "stat_band_1c47_word_5",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET + 16,
- ),
- (
- "stat_band_1c47_word_6",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET + 20,
- ),
- (
- "stat_band_1c47_word_7",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET + 24,
- ),
- (
- "stat_band_1c47_word_8",
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET + 28,
- ),
-];
const SAVE_COMPANY_RECORD_START_SCAN_LIMIT: usize = 0x120;
const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08;
@@ -3754,27 +3653,27 @@ fn parse_save_company_roster_probe(
bytes,
record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET,
)?;
- let stat_band_root_0cfb_candidates =
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_CANDIDATE_FIELDS
- .iter()
- .map(|(label, relative_offset)| {
- build_save_dword_candidate(bytes, record_offset, label, *relative_offset)
- })
- .collect:: >>()?;
- let stat_band_root_0d7f_candidates =
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_CANDIDATE_FIELDS
- .iter()
- .map(|(label, relative_offset)| {
- build_save_dword_candidate(bytes, record_offset, label, *relative_offset)
- })
- .collect:: >>()?;
- let stat_band_root_1c47_candidates =
- SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_CANDIDATE_FIELDS
- .iter()
- .map(|(label, relative_offset)| {
- build_save_dword_candidate(bytes, record_offset, label, *relative_offset)
- })
- .collect:: >>()?;
+ let stat_band_root_0cfb_candidates = build_save_company_stat_band_candidates(
+ bytes,
+ record_offset,
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET,
+ "stat_band_0cfb",
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
+ )?;
+ let stat_band_root_0d7f_candidates = build_save_company_stat_band_candidates(
+ bytes,
+ record_offset,
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET,
+ "stat_band_0d7f",
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
+ )?;
+ let stat_band_root_1c47_candidates = build_save_company_stat_band_candidates(
+ bytes,
+ record_offset,
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET,
+ "stat_band_1c47",
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
+ )?;
entries.push(SmpLoadedCompanyRosterEntry {
company_id,
active,
@@ -3848,6 +3747,22 @@ fn runtime_company_stat_band_candidate_from_save(
}
}
+fn build_save_company_stat_band_candidates(
+ bytes: &[u8],
+ record_offset: usize,
+ root_offset: usize,
+ label_prefix: &str,
+ word_count: usize,
+) -> Option> {
+ (0..word_count)
+ .map(|index| {
+ let relative_offset = root_offset.checked_add(index.checked_mul(4)?)?;
+ let label = format!("{label_prefix}_word_{}", index + 1);
+ build_save_dword_candidate(bytes, record_offset, &label, relative_offset)
+ })
+ .collect::>>()
+}
+
fn detect_save_company_record_start_offset(
bytes: &[u8],
header_probe: &SmpSaveTaggedCollectionHeaderProbe,
diff --git a/docs/README.md b/docs/README.md
index 2eb5675..9b98c67 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -116,7 +116,8 @@ The highest-value next passes are now:
latches, and the first grounded stat-band root windows at `[company+0x0cfb]`, `[company+0x0d7f]`,
and `[company+0x1c47]` for each live company, so later finance/stat-family readers can attach to
owned runtime data instead of one more guessed save offset; the first runtime-side `0x2329`
- stat-family reader seam is now also rehosted for slots `0x0d` and `0x1d`
+ stat-family reader seam is now also rehosted for slots `0x0d` and `0x1d`, and the saved
+ stat-band windows themselves now carry 16 dwords per root
- 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 19e578e..315118d 100644
--- a/docs/runtime-rehost-plan.md
+++ b/docs/runtime-rehost-plan.md
@@ -201,7 +201,8 @@ roster. The current owned company-side roster now includes not just the market/c
the first grounded stat-band root windows at `[company+0x0cfb]`, `[company+0x0d7f]`, and
`[company+0x1c47]`, and the first runtime-side `0x2329` stat-family reader seam is now rehosted
for slots `0x0d` and `0x1d`, so later finance readers can target saved owner state and one shared
-reader family directly.
+reader family directly. Those stat-band windows now carry 16 dwords per root in the save-slice and
+runtime-owned company market state.
## Why This Boundary