Probe stock building header families

This commit is contained in:
Jan Petykiewicz 2026-04-19 15:05:48 -07:00
commit 6c7ebb75b5
4 changed files with 131 additions and 0 deletions

View file

@ -21,6 +21,8 @@ pub struct BuildingTypeSourceFile {
pub byte_len: Option<usize>, pub byte_len: Option<usize>,
#[serde(default)] #[serde(default)]
pub bca_selector_probe: Option<BuildingTypeBcaSelectorProbe>, pub bca_selector_probe: Option<BuildingTypeBcaSelectorProbe>,
#[serde(default)]
pub bty_header_probe: Option<BuildingTypeBtyHeaderProbe>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -43,6 +45,26 @@ pub struct BuildingTypeBcaSelectorProbe {
pub byte_0xbb_hex: String, 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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildingTypeBcaSelectorPatternSummary { pub struct BuildingTypeBcaSelectorPatternSummary {
pub byte_len: usize, 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::Bca => Some(probe_bca_selector_bytes(&bytes)),
BuildingTypeSourceKind::Bty => None, 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( fn load_named_binding_comparison(
bindings_path: &Path, bindings_path: &Path,
entries: &[BuildingTypeSourceEntry], entries: &[BuildingTypeSourceEntry],
@ -416,6 +483,36 @@ mod tests {
assert_eq!(probe.byte_0xbb_hex, "0x78"); 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] #[test]
fn summarizes_recovered_table_families_from_entries_and_files() { fn summarizes_recovered_table_families_from_entries_and_files() {
let entries = vec![ let entries = vec![
@ -452,6 +549,7 @@ mod tests {
source_kind: BuildingTypeSourceKind::Bty, source_kind: BuildingTypeSourceKind::Bty,
byte_len: None, byte_len: None,
bca_selector_probe: None, bca_selector_probe: None,
bty_header_probe: None,
}, },
BuildingTypeSourceFile { BuildingTypeSourceFile {
file_name: "Warehouse.bca".to_string(), file_name: "Warehouse.bca".to_string(),
@ -460,6 +558,7 @@ mod tests {
source_kind: BuildingTypeSourceKind::Bca, source_kind: BuildingTypeSourceKind::Bca,
byte_len: None, byte_len: None,
bca_selector_probe: None, bca_selector_probe: None,
bty_header_probe: None,
}, },
]; ];

View file

@ -75,6 +75,15 @@
`Maintenance.bty` and `ServiceTower.bty`, alongside the style-specific house families. So the `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 recovered `0x005f3c6c/0x005f3c80` naming strip is now backed by the shipped on-disk
`BuildingTypes` filename families, not only by the loader-side disassembly. `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 The fixed
tail is explicit now too: `0x00444dd0` writes one direct dword from 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 `[world+0x19]`, one zeroed `0x1f4`-byte slab under `0x32cf`, closes the package, derives the

View file

@ -1307,6 +1307,17 @@
`0x005f3c6c/0x005f3c80` pair is no longer an anonymous bank-byte helper; it is the stock `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 style-family-to-combined-name resolver immediately below the remaining Tier-2 source-selection
frontier. 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 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 stores at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side
portrait/string refresh helpers over a different id-keyed collection rooted through portrait/string refresh helpers over a different id-keyed collection rooted through

View file

@ -843,6 +843,18 @@ Working rule:
`0x005f3c6c/0x005f3c80` pair is no longer an anonymous bank byte table; it is the stock `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 style-family-to-combined-name resolver that sits immediately below the remaining Tier-2 source
selection frontier. 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 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 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 refresh helpers: they walk a separate id-keyed collection through `0x0053f830`, free and