diff --git a/crates/rrt-runtime/src/building.rs b/crates/rrt-runtime/src/building.rs index abcd5c8..21eb82c 100644 --- a/crates/rrt-runtime/src/building.rs +++ b/crates/rrt-runtime/src/building.rs @@ -96,6 +96,7 @@ pub struct BuildingTypeRecoveredTableSummary { pub nonzero_bty_header_name_0x40_summaries: Vec, pub nonzero_bty_header_name_0x5e_summaries: Vec, pub nonzero_bty_header_name_0x7c_summaries: Vec, + pub bty_header_name_0x5e_dword_summaries: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -114,6 +115,16 @@ pub struct BuildingTypeBtyHeaderNameSummary { pub sample_file_names: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBtyHeaderNameDwordSummary { + pub header_offset_hex: String, + pub header_value: String, + pub dword_0xbb: u32, + pub dword_0xbb_hex: String, + pub file_count: usize, + pub sample_file_names: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BuildingTypeSourceReport { pub directory_path: String, @@ -483,6 +494,8 @@ fn summarize_recovered_table_families( summarize_nonzero_bty_header_name_lane(files, 0x5e, |probe| &probe.name_0x5e); let nonzero_bty_header_name_0x7c_summaries = summarize_nonzero_bty_header_name_lane(files, 0x7c, |probe| &probe.name_0x7c); + let bty_header_name_0x5e_dword_summaries = + summarize_bty_header_name_lane_by_dword(files, 0x5e, |probe| &probe.name_0x5e); BuildingTypeRecoveredTableSummary { recovered_style_themes: RECOVERED_STYLE_THEMES @@ -500,6 +513,7 @@ fn summarize_recovered_table_families( nonzero_bty_header_name_0x40_summaries, nonzero_bty_header_name_0x5e_summaries, nonzero_bty_header_name_0x7c_summaries, + bty_header_name_0x5e_dword_summaries, } } @@ -549,6 +563,52 @@ fn summarize_nonzero_bty_header_name_lane( summaries } +fn summarize_bty_header_name_lane_by_dword( + files: &[BuildingTypeSourceFile], + offset: u32, + selector: impl Fn(&BuildingTypeBtyHeaderProbe) -> &String, +) -> Vec { + let mut groups = BTreeMap::<(String, u32), Vec>::new(); + for file in files { + let Some(probe) = &file.bty_header_probe else { + continue; + }; + let header_value = selector(probe).trim(); + if header_value.is_empty() { + continue; + } + groups + .entry((header_value.to_string(), probe.dword_0xbb)) + .or_default() + .push(file.file_name.clone()); + } + + let mut summaries = groups + .into_iter() + .map(|((header_value, dword_0xbb), mut file_names)| { + file_names.sort(); + file_names.dedup(); + BuildingTypeBtyHeaderNameDwordSummary { + header_offset_hex: format!("0x{offset:02x}"), + header_value, + dword_0xbb, + dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), + file_count: file_names.len(), + sample_file_names: file_names.into_iter().take(24).collect(), + } + }) + .collect::>(); + summaries.sort_by(|left, right| { + right + .file_count + .cmp(&left.file_count) + .then_with(|| left.dword_0xbb.cmp(&right.dword_0xbb)) + .then_with(|| left.header_offset_hex.cmp(&right.header_offset_hex)) + .then_with(|| left.header_value.cmp(&right.header_value)) + }); + summaries +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] struct BuildingBindingArtifact { bindings: Vec, @@ -731,5 +791,16 @@ mod tests { sample_file_names: vec!["Port.bty".to_string()], }] ); + assert_eq!( + summary.bty_header_name_0x5e_dword_summaries, + vec![BuildingTypeBtyHeaderNameDwordSummary { + header_offset_hex: "0x5e".to_string(), + header_value: "TextileMill".to_string(), + dword_0xbb: 0x01f4, + dword_0xbb_hex: "0x000001f4".to_string(), + file_count: 1, + sample_file_names: vec!["Port.bty".to_string()], + }] + ); } } diff --git a/docs/control-loop-atlas/map-and-scenario-content-load.md b/docs/control-loop-atlas/map-and-scenario-content-load.md index 684cc08..5938310 100644 --- a/docs/control-loop-atlas/map-and-scenario-content-load.md +++ b/docs/control-loop-atlas/map-and-scenario-content-load.md @@ -96,6 +96,12 @@ `Distillery x2`, `Toolndie x2`). So the next load-side source-selection pass should bias toward that `0x5e` alias-root lane when testing why the later chooser seeds only part of the stock family into the numbered Tier-2 bank. + The same `name_0x5e` dword-family summary now also says the current residue is still stock-side: + `MunitionsFactory` belongs to a zero-valued `WeaponsFactory` alias cluster with + `Electric Plant`, `Fertilizer Factory`, `Nuclear Power Plant`, `Oil Well`, and `Weapons + Factory`. So the next load-side source-selection pass should treat the open question as one of + cluster choice inside the wider stock corpus, not as a jump from stock rows to some unrelated + non-stock family. The fixed tail is explicit now too: `0x00444dd0` writes one direct dword from `[world+0x19]`, one zeroed `0x1f4`-byte slab under `0x32cf`, closes the package, derives the diff --git a/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md b/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md index 0ae5ccf..f190eb2 100644 --- a/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md +++ b/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md @@ -1339,6 +1339,14 @@ stronger stock-family clue than the direct-name lanes, and keep the explicit non-overlap residue (`MunitionsFactory/MunitionsFactory x1`) separate instead of folding it into the recovered industrial/commercial subset. + That non-overlap residue is grounded against the wider stock corpus now too. The same checked-in + `name_0x5e` dword-family summary shows `MunitionsFactory` sits in a zero-valued + `WeaponsFactory` alias cluster (`Electric Plant`, `Fertilizer Factory`, `Munitions Factory`, + `Nuclear Power Plant`, `Oil Well`, `Weapons Factory`) rather than outside stock assets + entirely. So the remaining Tier-2 chooser/source-selection frontier is no longer “stock vs + non-stock”; it is which stock alias-root cluster is selected and why later clone/replay paths + prefer the nonzero `0x000001f4` cluster while the peer-site residue can still surface a + zero-family `WeaponsFactory`-side root. The direct `+0xba/+0xbb` writer census now rules out a broad false lead too. The obvious new stores at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side portrait/string refresh helpers over a different id-keyed collection rooted through diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index 88c37bd..1209b87 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -1285,6 +1285,15 @@ Working rule: chooser-side/source-selection slice can focus on whether that residue belongs to a zero-valued stock-header family or to a later live projection seam rather than treating the whole nonzero post-secondary set as one undifferentiated mystery + - that broader stock-header check is now grounded too: the checked-in `name_0x5e` dword-family + summary shows `MunitionsFactory` is not outside stock assets at all. It sits in a zero-valued + `WeaponsFactory` alias cluster (`Electric Plant`, `Fertilizer Factory`, `Munitions Factory`, + `Nuclear Power Plant`, `Oil Well`, `Weapons Factory`) while the recovered nonzero family keeps + its own `TextileMill`, `LumberMill`, `MeatPackingPlant`, `Distillery`, and `Toolndie` + clusters. So the next Tier-2 source-selection question is no longer “stock vs non-stock”; it + is which stock alias-root cluster gets selected, and why some later clone/replay paths prefer + the nonzero `0x000001f4` cluster while the peer-site residue can still surface a zero-family + `WeaponsFactory`-side root - keep the already-grounded `0x0047fd50` class gate separate from that byte: direct disassembly now says `0x0047fd50` resolves the linked peer through `[site+0x04]`, reads candidate class byte `[candidate+0x8c]`, and returns true only for `0/1/2` while rejecting `3/4` and above,