diff --git a/crates/rrt-runtime/src/building.rs b/crates/rrt-runtime/src/building.rs index 321f17e..1a3e4c4 100644 --- a/crates/rrt-runtime/src/building.rs +++ b/crates/rrt-runtime/src/building.rs @@ -21,6 +21,8 @@ pub struct BuildingTypeSourceFile { pub byte_len: Option, #[serde(default)] pub bca_selector_probe: Option, + #[serde(default)] + pub bty_header_probe: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -43,6 +45,26 @@ pub struct BuildingTypeBcaSelectorProbe { pub byte_0xbb_hex: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildingTypeBtyHeaderProbe { + pub type_id: u32, + pub type_id_hex: String, + pub name_0x04: String, + pub name_0x22: String, + pub name_0x40: String, + pub name_0x5e: String, + pub name_0x7c: String, + pub name_0x9a: String, + pub byte_0xb8: u8, + pub byte_0xb8_hex: String, + pub byte_0xb9: u8, + pub byte_0xb9_hex: String, + pub byte_0xba: u8, + pub byte_0xba_hex: String, + pub dword_0xbb: u32, + pub dword_0xbb_hex: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BuildingTypeBcaSelectorPatternSummary { pub byte_len: usize, @@ -136,6 +158,10 @@ pub fn inspect_building_types_dir_with_bindings( BuildingTypeSourceKind::Bca => Some(probe_bca_selector_bytes(&bytes)), BuildingTypeSourceKind::Bty => None, }, + bty_header_probe: match source_kind { + BuildingTypeSourceKind::Bca => None, + BuildingTypeSourceKind::Bty => Some(probe_bty_header(&bytes)), + }, }); } @@ -267,6 +293,47 @@ fn probe_bca_selector_bytes(bytes: &[u8]) -> BuildingTypeBcaSelectorProbe { } } +fn probe_bty_header(bytes: &[u8]) -> BuildingTypeBtyHeaderProbe { + let type_id = read_u32_le(bytes, 0x00); + let byte_0xb8 = bytes.get(0xb8).copied().unwrap_or(0); + let byte_0xb9 = bytes.get(0xb9).copied().unwrap_or(0); + let byte_0xba = bytes.get(0xba).copied().unwrap_or(0); + let dword_0xbb = read_u32_le(bytes, 0xbb); + BuildingTypeBtyHeaderProbe { + type_id, + type_id_hex: format!("0x{type_id:08x}"), + name_0x04: read_c_string(bytes, 0x04, 0x1e), + name_0x22: read_c_string(bytes, 0x22, 0x1e), + name_0x40: read_c_string(bytes, 0x40, 0x1e), + name_0x5e: read_c_string(bytes, 0x5e, 0x1e), + name_0x7c: read_c_string(bytes, 0x7c, 0x1e), + name_0x9a: read_c_string(bytes, 0x9a, 0x1e), + byte_0xb8, + byte_0xb8_hex: format!("0x{byte_0xb8:02x}"), + byte_0xb9, + byte_0xb9_hex: format!("0x{byte_0xb9:02x}"), + byte_0xba, + byte_0xba_hex: format!("0x{byte_0xba:02x}"), + dword_0xbb, + dword_0xbb_hex: format!("0x{dword_0xbb:08x}"), + } +} + +fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { + bytes.get(offset..offset + 4) + .and_then(|slice| <[u8; 4]>::try_from(slice).ok()) + .map(u32::from_le_bytes) + .unwrap_or(0) +} + +fn read_c_string(bytes: &[u8], offset: usize, max_len: usize) -> String { + let Some(slice) = bytes.get(offset..offset.saturating_add(max_len)) else { + return String::new(); + }; + let end = slice.iter().position(|byte| *byte == 0).unwrap_or(slice.len()); + String::from_utf8_lossy(&slice[..end]).into_owned() +} + fn load_named_binding_comparison( bindings_path: &Path, entries: &[BuildingTypeSourceEntry], @@ -416,6 +483,36 @@ mod tests { assert_eq!(probe.byte_0xbb_hex, "0x78"); } + #[test] + fn probes_bty_header_from_fixed_offsets() { + let mut bytes = vec![0u8; 0xc0]; + bytes[0x00..0x04].copy_from_slice(&0x03ebu32.to_le_bytes()); + bytes[0x04..0x04 + 5].copy_from_slice(b"Port\0"); + bytes[0x22..0x22 + 7].copy_from_slice(b"Cargo\0\0"); + bytes[0x40..0x40 + 6].copy_from_slice(b"Dock\0\0"); + bytes[0x5e..0x5e + 5].copy_from_slice(b"Sea\0\0"); + bytes[0x7c..0x7c + 6].copy_from_slice(b"Coast\0"); + bytes[0x9a..0x9a + 5].copy_from_slice(b"Port\0"); + bytes[0xb8] = 0x12; + bytes[0xb9] = 0x34; + bytes[0xba] = 0x56; + bytes[0xbb..0xbf].copy_from_slice(&0x89abcdefu32.to_le_bytes()); + + let probe = probe_bty_header(&bytes); + assert_eq!(probe.type_id, 0x03eb); + assert_eq!(probe.type_id_hex, "0x000003eb"); + assert_eq!(probe.name_0x04, "Port"); + assert_eq!(probe.name_0x22, "Cargo"); + assert_eq!(probe.name_0x40, "Dock"); + assert_eq!(probe.name_0x5e, "Sea"); + assert_eq!(probe.name_0x7c, "Coast"); + assert_eq!(probe.name_0x9a, "Port"); + assert_eq!(probe.byte_0xb8_hex, "0x12"); + assert_eq!(probe.byte_0xb9_hex, "0x34"); + assert_eq!(probe.byte_0xba_hex, "0x56"); + assert_eq!(probe.dword_0xbb_hex, "0x89abcdef"); + } + #[test] fn summarizes_recovered_table_families_from_entries_and_files() { let entries = vec![ @@ -452,6 +549,7 @@ mod tests { source_kind: BuildingTypeSourceKind::Bty, byte_len: None, bca_selector_probe: None, + bty_header_probe: None, }, BuildingTypeSourceFile { file_name: "Warehouse.bca".to_string(), @@ -460,6 +558,7 @@ mod tests { source_kind: BuildingTypeSourceKind::Bca, byte_len: None, bca_selector_probe: None, + bty_header_probe: None, }, ]; 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 fa780ec..b0de2cb 100644 --- a/docs/control-loop-atlas/map-and-scenario-content-load.md +++ b/docs/control-loop-atlas/map-and-scenario-content-load.md @@ -75,6 +75,15 @@ `Maintenance.bty` and `ServiceTower.bty`, alongside the style-specific house families. So the recovered `0x005f3c6c/0x005f3c80` naming strip is now backed by the shipped on-disk `BuildingTypes` filename families, not only by the loader-side disassembly. + The new `.bty` header probes now tighten that stock source split too: `Port.bty` and + `Warehouse.bty` both probe as ordinary `type_id = 0x000003ec` rows with direct bare-name + headers and shared `dword_0xbb = 0x000001f4`, while the style-station families such as + `VictorianStationSml/Med/Lrg.bty` stay on the same `0x000003ec` family but keep + `name_0x7c = VictorianStations` and zero `dword_0xbb`. `Maintenance.bty` and + `ServiceTower.bty` likewise stay in the same stock family, but expose display names + `Maintenance Facility` and `Service Tower` with zero `dword_0xbb`. So the later numbered + `Port%02d` / `Warehouse%02d` clone seam is now bounded above the bare `Port` / `Warehouse` + family itself rather than under a hidden station-style alias family in `BuildingTypes`. 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 a7b7500..3de3166 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 @@ -1307,6 +1307,17 @@ `0x005f3c6c/0x005f3c80` pair is no longer an anonymous bank-byte helper; it is the stock style-family-to-combined-name resolver immediately below the remaining Tier-2 source-selection frontier. + The recovered `.bty` header probes now sharpen that frontier too. `Port.bty` and + `Warehouse.bty` probe as ordinary `type_id = 0x000003ec` rows with direct bare-name headers + (`name_0x22` / `name_0x7c` = `Port` or `Warehouse`) and shared `dword_0xbb = 0x000001f4`, + while `VictorianStationSml/Med/Lrg.bty` stay on the same `0x000003ec` family but keep + `name_0x7c = VictorianStations` and `dword_0xbb = 0x00000000`. The standalone + `Maintenance.bty` / `ServiceTower.bty` rows also stay in that stock family, but their display + names are `Maintenance Facility` and `Service Tower` and their `dword_0xbb` lane remains zero. + So the numbered `Port%02d` / `Warehouse%02d` seam is no longer plausibly a hidden station-style + alias family under the stock assets; the remaining open question is why the later clone chooser + favors the bare `Port` / `Warehouse` family over those zero-valued station and maintenance / + service rows. 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 f1ba5f2..32c604a 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -843,6 +843,18 @@ Working rule: `0x005f3c6c/0x005f3c80` pair is no longer an anonymous bank byte table; it is the stock style-family-to-combined-name resolver that sits immediately below the remaining Tier-2 source selection frontier. + The recovered `.bty` header probes tighten that source split further too. The checked-in + `inspect-building-type-sources` report now shows `Port.bty` and `Warehouse.bty` are ordinary + `type_id = 0x000003ec` rows with direct bare-name headers (`name_0x22` / `name_0x7c` = + `Port` or `Warehouse`) and shared `dword_0xbb = 0x000001f4`, while the style-station rows such + as `VictorianStationSml/Med/Lrg.bty` stay on the same `0x000003ec` family but keep + `name_0x7c = VictorianStations` and `dword_0xbb = 0x00000000`. The standalone + `Maintenance.bty` / `ServiceTower.bty` rows also stay in the same stock family, but expose + display names `Maintenance Facility` and `Service Tower` with zero `dword_0xbb`. So the + remaining Tier-2 source question is no longer whether the numbered `Port%02d` / + `Warehouse%02d` banks are hidden station-style aliases; it is why the later clone path prefers + the bare `Port` / `Warehouse` family over the zero-valued station and maintenance/service + families when it seeds those numbered banks. The direct `+0xba/+0xbb` writer census is narrower now too. The obvious newly surfaced stores at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side portrait/string refresh helpers: they walk a separate id-keyed collection through `0x0053f830`, free and