Expose save-side region live directory

This commit is contained in:
Jan Petykiewicz 2026-04-18 08:18:05 -07:00
commit fa25433216
5 changed files with 265 additions and 22 deletions

View file

@ -150,9 +150,10 @@ company preference, ending it restores the base world byte, and runtime service
both the active and last applied override instead of treating the route-preference lane as a both the active and last applied override instead of treating the route-preference lane as a
reader-only bridge. reader-only bridge.
Save inspection now also exposes the tagged live region header Save inspection now also exposes the tagged live region header
`0x5209/0x520a/0x520b` and the tagged placed-structure header `0x36b1/0x36b2/0x36b3` as `0x5209/0x520a/0x520b` plus its live-entry directory rooted at metadata dword `16`, and the
first-class owner seams, so the remaining city-connection / linked-transit blocker is record-body tagged placed-structure header `0x36b1/0x36b2/0x36b3`, as first-class owner seams, so the
reconstruction rather than missing save-side collection identity. remaining city-connection / linked-transit blocker is record-body reconstruction rather than
missing save-side collection identity.
That same seam now also derives the current live coupon burden directly from owned bond slots, so That same seam now also derives the current live coupon burden directly from owned bond slots, so
later finance service work can consume a runtime reader instead of recomputing from scattered raw later finance service work can consume a runtime reader instead of recomputing from scattered raw
fields. fields.

View file

@ -150,6 +150,8 @@ const EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION: u32 = 0x000003e9;
const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19; const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19;
const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize = const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4;
const SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX: usize = 16;
const SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT: usize = 3;
const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001"; const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001";
const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1"; const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1";
const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1"; const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1";
@ -1611,6 +1613,38 @@ pub struct SmpSaveTaggedCollectionHeaderProbe {
pub evidence: Vec<String>, pub evidence: Vec<String>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveRegionCollectionDirectoryEntryProbe {
pub live_entry_id: u32,
pub payload_relative_offset: u32,
pub payload_relative_offset_hex: String,
pub payload_absolute_offset: usize,
pub previous_live_entry_id: u32,
pub previous_live_entry_id_hex: String,
pub next_live_entry_id: u32,
pub next_live_entry_id_hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpSaveRegionCollectionDirectoryProbe {
pub profile_family: String,
pub source_kind: String,
pub semantic_family: String,
pub metadata_tag_offset: usize,
pub records_tag_offset: usize,
pub close_tag_offset: usize,
pub directory_root_dword_index: usize,
pub directory_entry_dword_count: usize,
pub live_record_count: u32,
pub live_id_bound: u32,
#[serde(default)]
pub chain_head_live_entry_id: Option<u32>,
#[serde(default)]
pub chain_tail_live_entry_id: Option<u32>,
pub entries: Vec<SmpSaveRegionCollectionDirectoryEntryProbe>,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmpRt3105SaveNameTableProbe { pub struct SmpRt3105SaveNameTableProbe {
pub profile_family: String, pub profile_family: String,
@ -2547,6 +2581,8 @@ pub struct SmpSaveCompanyChairmanAnalysisReport {
#[serde(default)] #[serde(default)]
pub region_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>, pub region_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)] #[serde(default)]
pub region_collection_directory: Option<SmpSaveRegionCollectionDirectoryProbe>,
#[serde(default)]
pub placed_structure_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>, pub placed_structure_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)] #[serde(default)]
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>, pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
@ -2808,6 +2844,7 @@ pub struct SmpInspectionReport {
pub save_company_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>, pub save_company_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_chairman_profile_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>, pub save_chairman_profile_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_region_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>, pub save_region_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
pub save_region_collection_directory_probe: Option<SmpSaveRegionCollectionDirectoryProbe>,
pub save_placed_structure_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>, pub save_placed_structure_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
#[serde(default)] #[serde(default)]
pub save_company_roster_probe: Option<SmpLoadedCompanyRoster>, pub save_company_roster_probe: Option<SmpLoadedCompanyRoster>,
@ -3083,6 +3120,15 @@ pub fn load_save_slice_from_report(
probe.close_tag_offset probe.close_tag_offset
)); ));
} }
if let Some(probe) = &report.save_region_collection_directory_probe {
notes.push(format!(
"Raw save tagged region metadata also exposes a live-entry directory with {} entries rooted at metadata dword {} (head={:?}, tail={:?}).",
probe.entries.len(),
probe.directory_root_dword_index,
probe.chain_head_live_entry_id,
probe.chain_tail_live_entry_id
));
}
if let Some(probe) = &report.save_placed_structure_collection_header_probe { if let Some(probe) = &report.save_placed_structure_collection_header_probe {
notes.push(format!( notes.push(format!(
"Raw save tagged placed-structure header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.", "Raw save tagged placed-structure header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
@ -3154,6 +3200,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
let world_issue_37 = report.save_world_issue_37_probe.clone(); let world_issue_37 = report.save_world_issue_37_probe.clone();
let world_economic_tuning = report.save_world_economic_tuning_probe.clone(); let world_economic_tuning = report.save_world_economic_tuning_probe.clone();
let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone(); let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone();
let region_collection_directory = report.save_region_collection_directory_probe.clone();
let company_header_probe = report.save_company_collection_header_probe.as_ref(); let company_header_probe = report.save_company_collection_header_probe.as_ref();
let chairman_header_probe = report let chairman_header_probe = report
.save_chairman_profile_collection_header_probe .save_chairman_profile_collection_header_probe
@ -3449,6 +3496,15 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
header.live_record_count, header.live_id_bound, header.direct_record_stride header.live_record_count, header.live_id_bound, header.direct_record_stride
)); ));
} }
if let Some(directory) = region_collection_directory.as_ref() {
notes.push(format!(
"Region analysis now also exports the tagged live-entry directory rooted at metadata dword {}: {} entries chained from {:?} to {:?}.",
directory.directory_root_dword_index,
directory.entries.len(),
directory.chain_head_live_entry_id,
directory.chain_tail_live_entry_id
));
}
if let Some(header) = report if let Some(header) = report
.save_placed_structure_collection_header_probe .save_placed_structure_collection_header_probe
.as_ref() .as_ref()
@ -3500,6 +3556,7 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
world_economic_tuning, world_economic_tuning,
world_finance_neighborhood, world_finance_neighborhood,
region_collection_header: report.save_region_collection_header_probe.clone(), region_collection_header: report.save_region_collection_header_probe.clone(),
region_collection_directory,
placed_structure_collection_header: report placed_structure_collection_header: report
.save_placed_structure_collection_header_probe .save_placed_structure_collection_header_probe
.clone(), .clone(),
@ -7506,6 +7563,10 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
file_extension_hint.as_deref(), file_extension_hint.as_deref(),
container_profile.as_ref(), container_profile.as_ref(),
); );
let save_region_collection_directory_probe = parse_save_region_collection_directory_probe(
bytes,
save_region_collection_header_probe.as_ref(),
);
let save_placed_structure_collection_header_probe = let save_placed_structure_collection_header_probe =
parse_save_placed_structure_collection_header_probe( parse_save_placed_structure_collection_header_probe(
bytes, bytes,
@ -7676,6 +7737,7 @@ fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> Sm
save_company_collection_header_probe, save_company_collection_header_probe,
save_chairman_profile_collection_header_probe, save_chairman_profile_collection_header_probe,
save_region_collection_header_probe, save_region_collection_header_probe,
save_region_collection_directory_probe,
save_placed_structure_collection_header_probe, save_placed_structure_collection_header_probe,
save_company_roster_probe, save_company_roster_probe,
save_chairman_profile_table_probe, save_chairman_profile_table_probe,
@ -9645,6 +9707,86 @@ fn parse_save_region_collection_header_probe(
) )
} }
fn parse_save_region_collection_directory_probe(
bytes: &[u8],
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
) -> Option<SmpSaveRegionCollectionDirectoryProbe> {
let header_probe = header_probe?;
if header_probe.source_kind != "save-region-tagged-header-counts" {
return None;
}
let metadata_payload =
bytes.get(header_probe.metadata_tag_offset + 4..header_probe.records_tag_offset)?;
let directory_root_byte_offset =
SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX.checked_mul(4)?;
let live_record_count = header_probe.live_record_count as usize;
let directory_len_dwords =
live_record_count.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT)?;
let directory_len_bytes = directory_len_dwords.checked_mul(4)?;
let directory_bytes = metadata_payload.get(
directory_root_byte_offset..directory_root_byte_offset.checked_add(directory_len_bytes)?,
)?;
let mut entries = Vec::with_capacity(live_record_count);
for index in 0..live_record_count {
let entry_offset =
index.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT * 4)?;
let payload_relative_offset = read_u32_at(directory_bytes, entry_offset)?;
let previous_live_entry_id = read_u32_at(directory_bytes, entry_offset + 4)?;
let next_live_entry_id = read_u32_at(directory_bytes, entry_offset + 8)?;
entries.push(SmpSaveRegionCollectionDirectoryEntryProbe {
live_entry_id: (index + 1) as u32,
payload_relative_offset,
payload_relative_offset_hex: format!("0x{payload_relative_offset:08x}"),
payload_absolute_offset: header_probe
.metadata_tag_offset
.checked_add(4)?
.checked_add(payload_relative_offset as usize)?,
previous_live_entry_id,
previous_live_entry_id_hex: format!("0x{previous_live_entry_id:08x}"),
next_live_entry_id,
next_live_entry_id_hex: format!("0x{next_live_entry_id:08x}"),
});
}
let chain_head_live_entry_id = entries
.iter()
.find(|entry| entry.previous_live_entry_id == 0)
.map(|entry| entry.live_entry_id);
let chain_tail_live_entry_id = entries
.iter()
.find(|entry| entry.next_live_entry_id == 0)
.map(|entry| entry.live_entry_id);
let monotonic_offsets = entries
.windows(2)
.all(|window| window[0].payload_relative_offset < window[1].payload_relative_offset);
Some(SmpSaveRegionCollectionDirectoryProbe {
profile_family: header_probe.profile_family.clone(),
source_kind: "save-region-live-directory".to_string(),
semantic_family: "scenario-save-region-live-directory".to_string(),
metadata_tag_offset: header_probe.metadata_tag_offset,
records_tag_offset: header_probe.records_tag_offset,
close_tag_offset: header_probe.close_tag_offset,
directory_root_dword_index: SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX,
directory_entry_dword_count: SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT,
live_record_count: header_probe.live_record_count,
live_id_bound: header_probe.live_id_bound,
chain_head_live_entry_id,
chain_tail_live_entry_id,
entries,
evidence: vec![
"save-side region metadata payload exposes a live-entry directory immediately after the first 16 dwords, before the records tag".to_string(),
format!(
"region live directory decodes {} triplets of (payload_relative_offset, prev_live_entry_id, next_live_entry_id) from metadata dword index {}",
header_probe.live_record_count,
SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX
),
format!(
"decoded directory preserves a head/tail chain {:?}->{:?} and monotonic payload offsets={monotonic_offsets}",
chain_head_live_entry_id, chain_tail_live_entry_id
),
],
})
}
fn parse_save_placed_structure_collection_header_probe( fn parse_save_placed_structure_collection_header_probe(
bytes: &[u8], bytes: &[u8],
file_extension_hint: Option<&str>, file_extension_hint: Option<&str>,
@ -16541,6 +16683,44 @@ mod tests {
], ],
evidence: vec![], evidence: vec![],
}); });
report.save_region_collection_directory_probe =
Some(SmpSaveRegionCollectionDirectoryProbe {
profile_family: "rt3-105-save-container-v1".to_string(),
source_kind: "save-region-live-directory".to_string(),
semantic_family: "scenario-save-region-live-directory".to_string(),
metadata_tag_offset: 0x3000,
records_tag_offset: 0x3100,
close_tag_offset: 0x3200,
directory_root_dword_index: 16,
directory_entry_dword_count: 3,
live_record_count: 0x14,
live_id_bound: 0x32,
chain_head_live_entry_id: Some(1),
chain_tail_live_entry_id: Some(20),
entries: vec![
SmpSaveRegionCollectionDirectoryEntryProbe {
live_entry_id: 1,
payload_relative_offset: 0x2af8,
payload_relative_offset_hex: "0x00002af8".to_string(),
payload_absolute_offset: 0x5afc,
previous_live_entry_id: 0,
previous_live_entry_id_hex: "0x00000000".to_string(),
next_live_entry_id: 2,
next_live_entry_id_hex: "0x00000002".to_string(),
},
SmpSaveRegionCollectionDirectoryEntryProbe {
live_entry_id: 2,
payload_relative_offset: 0x2ee0,
payload_relative_offset_hex: "0x00002ee0".to_string(),
payload_absolute_offset: 0x5ee4,
previous_live_entry_id: 1,
previous_live_entry_id_hex: "0x00000001".to_string(),
next_live_entry_id: 0,
next_live_entry_id_hex: "0x00000000".to_string(),
},
],
evidence: vec![],
});
report.save_placed_structure_collection_header_probe = report.save_placed_structure_collection_header_probe =
Some(SmpSaveTaggedCollectionHeaderProbe { Some(SmpSaveTaggedCollectionHeaderProbe {
profile_family: "rt3-105-save-container-v1".to_string(), profile_family: "rt3-105-save-container-v1".to_string(),
@ -16640,6 +16820,9 @@ mod tests {
.iter() .iter()
.any(|note| note.contains("tagged region header reports live_record_count=20")) .any(|note| note.contains("tagged region header reports live_record_count=20"))
); );
assert!(slice.notes.iter().any(|note| {
note.contains("tagged region metadata also exposes a live-entry directory")
}));
assert!(slice.notes.iter().any(|note| { assert!(slice.notes.iter().any(|note| {
note.contains("tagged placed-structure header reports live_record_count=2026") note.contains("tagged placed-structure header reports live_record_count=2026")
})); }));
@ -16760,6 +16943,62 @@ mod tests {
assert_eq!(probe.live_record_count, 0x14); assert_eq!(probe.live_record_count, 0x14);
} }
#[test]
fn parses_region_collection_directory_probe_from_tagged_metadata_triplets() {
let mut bytes = vec![0u8; 0x400];
let metadata_tag_offset = 0x40usize;
let records_tag_offset = 0x180usize;
let close_tag_offset = 0x1c0usize;
bytes[metadata_tag_offset..metadata_tag_offset + 4]
.copy_from_slice(&0x00005209u32.to_le_bytes());
bytes[records_tag_offset..records_tag_offset + 4]
.copy_from_slice(&0x0000520au32.to_le_bytes());
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
let header_words = [
1u32, 0x1d5, 0x32, 0x03, 0x32, 0x03, 0x14, 1, 0, 0, 1, 1, 0x14, 0, 0, 0,
];
for (index, word) in header_words.into_iter().enumerate() {
let offset = metadata_tag_offset + 4 + index * 4;
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
let triplets = [(0x2af8u32, 0u32, 2u32), (0x2ee0, 1, 3), (0x32c8, 2, 0)];
for (index, (offset_word, prev, next)) in triplets.into_iter().enumerate() {
let base = metadata_tag_offset
+ 4
+ (SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX + index * 3) * 4;
bytes[base..base + 4].copy_from_slice(&offset_word.to_le_bytes());
bytes[base + 4..base + 8].copy_from_slice(&prev.to_le_bytes());
bytes[base + 8..base + 12].copy_from_slice(&next.to_le_bytes());
}
let header_probe = parse_save_region_collection_header_probe(
&bytes,
Some("gms"),
Some(&SmpContainerProfile {
profile_family: "rt3-105-save-container-v1".to_string(),
profile_evidence: vec![],
is_known_profile: true,
}),
)
.expect("region header probe should parse");
let directory_probe =
parse_save_region_collection_directory_probe(&bytes, Some(&header_probe))
.expect("region directory probe should parse");
assert_eq!(directory_probe.directory_root_dword_index, 16);
assert_eq!(directory_probe.live_record_count, 3);
assert_eq!(directory_probe.chain_head_live_entry_id, Some(1));
assert_eq!(directory_probe.chain_tail_live_entry_id, Some(3));
assert_eq!(directory_probe.entries.len(), 3);
assert_eq!(directory_probe.entries[0].live_entry_id, 1);
assert_eq!(directory_probe.entries[0].payload_relative_offset, 0x2af8);
assert_eq!(directory_probe.entries[0].previous_live_entry_id, 0);
assert_eq!(directory_probe.entries[0].next_live_entry_id, 2);
assert_eq!(directory_probe.entries[2].live_entry_id, 3);
assert_eq!(directory_probe.entries[2].previous_live_entry_id, 2);
assert_eq!(directory_probe.entries[2].next_live_entry_id, 0);
}
#[test] #[test]
fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() { fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() {
let mut bytes = vec![0u8; 0x400]; let mut bytes = vec![0u8; 0x400];

View file

@ -191,10 +191,11 @@ The highest-value next passes are now:
electric-only periodic-company override rewrites the world route-preference byte to the electric-only periodic-company override rewrites the world route-preference byte to the
effective company preference, ending it restores the base world byte, and runtime service state effective company preference, ending it restores the base world byte, and runtime service state
now carries both the active and last applied override now carries both the active and last applied override
- save inspection now also exposes the tagged live region header `0x5209/0x520a/0x520b` and the - save inspection now also exposes the tagged live region header `0x5209/0x520a/0x520b` plus its
tagged placed-structure header `0x36b1/0x36b2/0x36b3` as first-class owner seams, so the live-entry directory rooted at metadata dword `16`, and the tagged placed-structure header
remaining city-connection / linked-transit blocker is record-body reconstruction rather than `0x36b1/0x36b2/0x36b3`, as first-class owner seams, so the remaining city-connection /
missing save-side collection identity linked-transit blocker is record-body reconstruction rather than missing save-side collection
identity
- the project rule on the remaining closure work is now explicit too: when one runtime-facing field - 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 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; the checked-in instead of guessing another derived leaf field from neighboring raw offsets; the checked-in

View file

@ -9,10 +9,11 @@ Working rule:
## Next ## Next
- Reconstruct the save-side region record body on top of the newly grounded tagged region header, - Reconstruct the save-side region record body on top of the newly grounded tagged region
especially the pending bonus lane `[region+0x276]`, completion latch `[region+0x302]`, one-shot header-plus-directory seam, especially the pending bonus lane `[region+0x276]`, completion latch
notice latch `[region+0x316]`, severity/source lane `[region+0x25e]`, and any stable region-id `[region+0x302]`, one-shot notice latch `[region+0x316]`, severity/source lane
or class discriminator that can drive shellless city-connection service. `[region+0x25e]`, and any stable region-id or class discriminator that can drive shellless
city-connection service.
- Reconstruct the save-side placed-structure collection body on top of the newly grounded - Reconstruct the save-side placed-structure collection body on top of the newly grounded
`0x36b1/0x36b2/0x36b3` header seam so the blocked city-connection / linked-transit branch can `0x36b1/0x36b2/0x36b3` header seam so the blocked city-connection / linked-transit branch can
stop depending on atlas-only placed-structure and local-runtime refresh notes. stop depending on atlas-only placed-structure and local-runtime refresh notes.
@ -43,17 +44,17 @@ Working rule:
- Any candidate slice that requires guessing rather than rehosting owning state or real - Any candidate slice that requires guessing rather than rehosting owning state or real
reader/setter families stays blocked until a better owner seam is grounded. reader/setter families stays blocked until a better owner seam is grounded.
- The city-connection announcement / linked-transit roster-maintenance branch is still blocked at - The city-connection announcement / linked-transit roster-maintenance branch is still blocked at
the record-body level, not the header level: the runtime now has tagged region and the record-body level, not the collection-identity level: the runtime now has a tagged region
placed-structure collection header seams, but it does not yet reconstruct the live region or header-plus-directory seam and a tagged placed-structure header seam, but it does not yet
placed-structure record bodies those service owners need. reconstruct the live region or placed-structure record bodies those service owners need.
## Recently Done ## Recently Done
- Save inspection now exposes the tagged region collection header (`0x5209/0x520a/0x520b`, - Save inspection now exposes the tagged region collection header (`0x5209/0x520a/0x520b`,
stride `0x1d5`) and the tagged placed-structure collection header stride `0x1d5`) plus the live-entry directory rooted at metadata dword `16`, and the tagged
(`0x36b1/0x36b2/0x36b3`) as first-class owner seams, so the blocked city-connection / placed-structure collection header (`0x36b1/0x36b2/0x36b3`) as first-class owner seams, so the
linked-transit queue now has grounded save-side collection counts and ids to build on instead of blocked city-connection / linked-transit queue now has grounded save-side collection counts,
only atlas notes. ids, and region payload offsets to build on instead of only atlas notes.
- Stepped calendar progression now also refreshes save-world owner time fields, including packed - Stepped calendar progression now also refreshes save-world owner time fields, including packed
year, packed tuple words, absolute counter, and the derived selected-year gap scalar. year, packed tuple words, absolute counter, and the derived selected-year gap scalar.
- Automatic year-rollover calendar stepping now invokes periodic-boundary service. - Automatic year-rollover calendar stepping now invokes periodic-boundary service.

View file

@ -269,10 +269,11 @@ electric-only periodic-company override rewrites `[world+0x4c74]` to the effecti
preference for the active service pass, ending the override restores the base world byte, and preference for the active service pass, ending the override restores the base world byte, and
runtime service state now carries both the active and last applied override instead of leaving the runtime service state now carries both the active and last applied override instead of leaving the
route-preference seam as a pure reader note. route-preference seam as a pure reader note.
Save inspection now also exposes the tagged live region header `0x5209/0x520a/0x520b` and the Save inspection now also exposes the tagged live region header `0x5209/0x520a/0x520b` plus its
tagged placed-structure header `0x36b1/0x36b2/0x36b3` as first-class owner seams, so the live-entry directory rooted at metadata dword `16`, and the tagged placed-structure header
remaining city-connection / linked-transit blocker is record-body reconstruction rather than `0x36b1/0x36b2/0x36b3`, as first-class owner seams, so the remaining city-connection /
missing save-side collection identity. linked-transit blocker is record-body reconstruction rather than missing save-side collection
identity.
That same seam now also carries the fixed-world building-density growth setting plus the linked 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 chairman personality byte, which is enough to rehost the annual stock-repurchase gate on owned
save/runtime state instead of another threshold-only note. The stock-capital issue branch now save/runtime state instead of another threshold-only note. The stock-capital issue branch now