2026-04-19 02:56:10 -07:00
use std ::collections ::{ BTreeMap , BTreeSet } ;
use std ::fs ;
use std ::path ::Path ;
use serde ::{ Deserialize , Serialize } ;
#[ derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize) ]
#[ serde(rename_all = " snake_case " ) ]
pub enum BuildingTypeSourceKind {
Bca ,
Bty ,
}
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeSourceFile {
pub file_name : String ,
pub raw_stem : String ,
pub canonical_stem : String ,
pub source_kind : BuildingTypeSourceKind ,
2026-04-19 12:11:58 -07:00
#[ serde(default) ]
pub byte_len : Option < usize > ,
#[ serde(default) ]
pub bca_selector_probe : Option < BuildingTypeBcaSelectorProbe > ,
2026-04-19 15:05:48 -07:00
#[ serde(default) ]
pub bty_header_probe : Option < BuildingTypeBtyHeaderProbe > ,
2026-04-19 02:56:10 -07:00
}
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeSourceEntry {
pub canonical_stem : String ,
pub raw_stems : Vec < String > ,
pub source_kinds : Vec < BuildingTypeSourceKind > ,
pub file_names : Vec < String > ,
}
2026-04-19 12:11:58 -07:00
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeBcaSelectorProbe {
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 byte_0xbb : u8 ,
pub byte_0xbb_hex : String ,
}
2026-04-19 15:05:48 -07:00
#[ 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 ,
}
2026-04-19 12:11:58 -07:00
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeBcaSelectorPatternSummary {
pub byte_len : usize ,
pub byte_0xb8_hex : String ,
pub byte_0xb9_hex : String ,
pub byte_0xba_hex : String ,
pub byte_0xbb_hex : String ,
pub file_count : usize ,
pub sample_file_names : Vec < String > ,
}
2026-04-19 03:02:11 -07:00
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeNamedBindingComparison {
pub bindings_path : String ,
pub named_binding_count : usize ,
pub shared_canonical_stem_count : usize ,
pub binding_only_canonical_stems : Vec < String > ,
pub source_only_canonical_stems : Vec < String > ,
}
2026-04-19 14:58:27 -07:00
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeRecoveredTableSummary {
pub recovered_style_themes : Vec < String > ,
pub recovered_source_kinds : Vec < String > ,
pub present_style_station_entries : Vec < String > ,
pub present_standalone_entries : Vec < String > ,
pub bare_port_warehouse_files : Vec < String > ,
2026-04-19 15:09:09 -07:00
pub nonzero_bty_header_dword_summaries : Vec < BuildingTypeBtyHeaderDwordSummary > ,
2026-04-19 15:28:23 -07:00
pub nonzero_bty_header_name_0x40_summaries : Vec < BuildingTypeBtyHeaderNameSummary > ,
pub nonzero_bty_header_name_0x5e_summaries : Vec < BuildingTypeBtyHeaderNameSummary > ,
pub nonzero_bty_header_name_0x7c_summaries : Vec < BuildingTypeBtyHeaderNameSummary > ,
2026-04-19 15:09:09 -07:00
}
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeBtyHeaderDwordSummary {
pub dword_0xbb : u32 ,
pub dword_0xbb_hex : String ,
pub file_count : usize ,
pub sample_file_names : Vec < String > ,
2026-04-19 14:58:27 -07:00
}
2026-04-19 15:28:23 -07:00
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeBtyHeaderNameSummary {
pub header_offset_hex : String ,
pub header_value : String ,
pub file_count : usize ,
pub sample_file_names : Vec < String > ,
}
2026-04-19 02:56:10 -07:00
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
pub struct BuildingTypeSourceReport {
pub directory_path : String ,
pub bca_file_count : usize ,
pub bty_file_count : usize ,
pub unique_canonical_stem_count : usize ,
2026-04-19 12:11:58 -07:00
pub bca_selector_pattern_count : usize ,
2026-04-19 03:02:11 -07:00
#[ serde(default) ]
pub named_binding_comparison : Option < BuildingTypeNamedBindingComparison > ,
2026-04-19 14:58:27 -07:00
pub recovered_table_summary : BuildingTypeRecoveredTableSummary ,
2026-04-19 02:56:10 -07:00
pub notes : Vec < String > ,
2026-04-19 12:11:58 -07:00
pub bca_selector_patterns : Vec < BuildingTypeBcaSelectorPatternSummary > ,
2026-04-19 02:56:10 -07:00
pub files : Vec < BuildingTypeSourceFile > ,
pub entries : Vec < BuildingTypeSourceEntry > ,
}
pub fn inspect_building_types_dir (
path : & Path ,
2026-04-19 03:02:11 -07:00
) -> Result < BuildingTypeSourceReport , Box < dyn std ::error ::Error > > {
inspect_building_types_dir_with_bindings ( path , None )
}
pub fn inspect_building_types_dir_with_bindings (
path : & Path ,
bindings_path : Option < & Path > ,
2026-04-19 02:56:10 -07:00
) -> Result < BuildingTypeSourceReport , Box < dyn std ::error ::Error > > {
let mut files = Vec ::new ( ) ;
for entry in fs ::read_dir ( path ) ? {
let entry = entry ? ;
if ! entry . file_type ( ) ? . is_file ( ) {
continue ;
}
let file_name = entry . file_name ( ) . to_string_lossy ( ) . into_owned ( ) ;
let Some ( extension ) = Path ::new ( & file_name )
. extension ( )
. and_then ( | extension | extension . to_str ( ) )
. map ( | extension | extension . to_ascii_lowercase ( ) )
else {
continue ;
} ;
let source_kind = match extension . as_str ( ) {
" bca " = > BuildingTypeSourceKind ::Bca ,
" bty " = > BuildingTypeSourceKind ::Bty ,
_ = > continue ,
} ;
2026-04-19 12:11:58 -07:00
let bytes = fs ::read ( entry . path ( ) ) ? ;
2026-04-19 02:56:10 -07:00
let raw_stem = Path ::new ( & file_name )
. file_stem ( )
. and_then ( | stem | stem . to_str ( ) )
. unwrap_or ( " " )
. to_string ( ) ;
if raw_stem . is_empty ( ) {
continue ;
}
files . push ( BuildingTypeSourceFile {
file_name ,
canonical_stem : canonicalize_building_stem ( & raw_stem ) ,
raw_stem ,
2026-04-19 12:11:58 -07:00
source_kind : source_kind . clone ( ) ,
byte_len : Some ( bytes . len ( ) ) ,
bca_selector_probe : match source_kind {
BuildingTypeSourceKind ::Bca = > Some ( probe_bca_selector_bytes ( & bytes ) ) ,
BuildingTypeSourceKind ::Bty = > None ,
} ,
2026-04-19 15:05:48 -07:00
bty_header_probe : match source_kind {
BuildingTypeSourceKind ::Bca = > None ,
BuildingTypeSourceKind ::Bty = > Some ( probe_bty_header ( & bytes ) ) ,
} ,
2026-04-19 02:56:10 -07:00
} ) ;
}
files . sort_by ( | left , right | {
left . canonical_stem
. cmp ( & right . canonical_stem )
. then_with ( | | left . file_name . cmp ( & right . file_name ) )
} ) ;
let mut grouped = BTreeMap ::< String , Vec < & BuildingTypeSourceFile > > ::new ( ) ;
for file in & files {
grouped
. entry ( file . canonical_stem . clone ( ) )
. or_default ( )
. push ( file ) ;
}
let entries = grouped
. into_iter ( )
. map ( | ( canonical_stem , group ) | BuildingTypeSourceEntry {
canonical_stem ,
raw_stems : group
. iter ( )
. map ( | file | file . raw_stem . clone ( ) )
. collect ::< BTreeSet < _ > > ( )
. into_iter ( )
. collect ( ) ,
source_kinds : group
. iter ( )
. map ( | file | file . source_kind . clone ( ) )
. collect ::< BTreeSet < _ > > ( )
. into_iter ( )
. collect ( ) ,
file_names : group
. iter ( )
. map ( | file | file . file_name . clone ( ) )
. collect ::< BTreeSet < _ > > ( )
. into_iter ( )
. collect ( ) ,
} )
. collect ::< Vec < _ > > ( ) ;
let bca_file_count = files
. iter ( )
. filter ( | file | matches! ( file . source_kind , BuildingTypeSourceKind ::Bca ) )
. count ( ) ;
let bty_file_count = files
. iter ( )
. filter ( | file | matches! ( file . source_kind , BuildingTypeSourceKind ::Bty ) )
. count ( ) ;
2026-04-19 12:11:58 -07:00
let mut grouped_selector_patterns =
BTreeMap ::< ( usize , String , String , String , String ) , Vec < String > > ::new ( ) ;
for file in & files {
let Some ( probe ) = & file . bca_selector_probe else {
continue ;
} ;
grouped_selector_patterns
. entry ( (
file . byte_len . unwrap_or_default ( ) ,
probe . byte_0xb8_hex . clone ( ) ,
probe . byte_0xb9_hex . clone ( ) ,
probe . byte_0xba_hex . clone ( ) ,
probe . byte_0xbb_hex . clone ( ) ,
) )
. or_default ( )
. push ( file . file_name . clone ( ) ) ;
}
let bca_selector_patterns = grouped_selector_patterns
. into_iter ( )
. map (
| (
( byte_len , byte_0xb8_hex , byte_0xb9_hex , byte_0xba_hex , byte_0xbb_hex ) ,
file_names ,
) | BuildingTypeBcaSelectorPatternSummary {
byte_len ,
byte_0xb8_hex ,
byte_0xb9_hex ,
byte_0xba_hex ,
byte_0xbb_hex ,
file_count : file_names . len ( ) ,
sample_file_names : file_names . into_iter ( ) . take ( 12 ) . collect ( ) ,
} ,
)
. collect ::< Vec < _ > > ( ) ;
2026-04-19 02:56:10 -07:00
let notes = vec! [
" BuildingTypes sources are grouped by a canonical stem that lowercases and strips spaces, underscores, and hyphens so paired .bca/.bty variants collapse onto one asset token. " . to_string ( ) ,
" This report is an offline asset-pool view only; it does not by itself assign live candidate ids or prove scenario candidate-table availability. " . to_string ( ) ,
2026-04-19 12:11:58 -07:00
" For .bca files, the report also exposes the narrow selector-byte window at offsets 0xb8..0xbb used by the grounded aux-candidate and live-candidate stream decoders. " . to_string ( ) ,
2026-04-19 14:58:27 -07:00
" The recovered stock table above the Tier-2 building seam combines one style/theme subset with one source-kind table; this report now surfaces the matching on-disk filename families directly. " . to_string ( ) ,
2026-04-19 02:56:10 -07:00
] ;
2026-04-19 03:02:11 -07:00
let named_binding_comparison = if let Some ( bindings_path ) = bindings_path {
Some ( load_named_binding_comparison ( bindings_path , & entries ) ? )
} else {
None
} ;
2026-04-19 14:58:27 -07:00
let recovered_table_summary = summarize_recovered_table_families ( & entries , & files ) ;
2026-04-19 03:02:11 -07:00
2026-04-19 02:56:10 -07:00
Ok ( BuildingTypeSourceReport {
directory_path : path . display ( ) . to_string ( ) ,
bca_file_count ,
bty_file_count ,
unique_canonical_stem_count : entries . len ( ) ,
2026-04-19 12:11:58 -07:00
bca_selector_pattern_count : bca_selector_patterns . len ( ) ,
2026-04-19 03:02:11 -07:00
named_binding_comparison ,
2026-04-19 14:58:27 -07:00
recovered_table_summary ,
2026-04-19 02:56:10 -07:00
notes ,
2026-04-19 12:11:58 -07:00
bca_selector_patterns ,
2026-04-19 02:56:10 -07:00
files ,
entries ,
} )
}
2026-04-19 12:11:58 -07:00
fn probe_bca_selector_bytes ( bytes : & [ u8 ] ) -> BuildingTypeBcaSelectorProbe {
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 byte_0xbb = bytes . get ( 0xbb ) . copied ( ) . unwrap_or ( 0 ) ;
BuildingTypeBcaSelectorProbe {
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} " ) ,
byte_0xbb ,
byte_0xbb_hex : format ! ( " 0x{byte_0xbb:02x} " ) ,
}
}
2026-04-19 15:05:48 -07:00
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 {
2026-04-19 15:28:23 -07:00
bytes
. get ( offset .. offset + 4 )
2026-04-19 15:05:48 -07:00
. 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 ( ) ;
} ;
2026-04-19 15:28:23 -07:00
let end = slice
. iter ( )
. position ( | byte | * byte = = 0 )
. unwrap_or ( slice . len ( ) ) ;
2026-04-19 15:05:48 -07:00
String ::from_utf8_lossy ( & slice [ .. end ] ) . into_owned ( )
}
2026-04-19 03:02:11 -07:00
fn load_named_binding_comparison (
bindings_path : & Path ,
entries : & [ BuildingTypeSourceEntry ] ,
) -> Result < BuildingTypeNamedBindingComparison , Box < dyn std ::error ::Error > > {
let artifact =
serde_json ::from_str ::< BuildingBindingArtifact > ( & fs ::read_to_string ( bindings_path ) ? ) ? ;
let named_binding_stems = artifact
. bindings
. into_iter ( )
. filter_map ( | binding | binding . candidate_name )
. map ( | candidate_name | canonicalize_building_stem ( & candidate_name ) )
. collect ::< BTreeSet < _ > > ( ) ;
let source_stems = entries
. iter ( )
. map ( | entry | entry . canonical_stem . clone ( ) )
. collect ::< BTreeSet < _ > > ( ) ;
Ok ( BuildingTypeNamedBindingComparison {
bindings_path : bindings_path . display ( ) . to_string ( ) ,
named_binding_count : named_binding_stems . len ( ) ,
shared_canonical_stem_count : named_binding_stems . intersection ( & source_stems ) . count ( ) ,
binding_only_canonical_stems : named_binding_stems
. difference ( & source_stems )
. cloned ( )
. collect ( ) ,
source_only_canonical_stems : source_stems
. difference ( & named_binding_stems )
. cloned ( )
. collect ( ) ,
} )
}
2026-04-19 02:56:10 -07:00
fn canonicalize_building_stem ( stem : & str ) -> String {
stem . chars ( )
. filter ( | ch | ! matches! ( ch , ' ' | '_' | '-' ) )
. flat_map ( | ch | ch . to_lowercase ( ) )
. collect ( )
}
2026-04-19 03:02:11 -07:00
2026-04-19 14:58:27 -07:00
fn summarize_recovered_table_families (
entries : & [ BuildingTypeSourceEntry ] ,
files : & [ BuildingTypeSourceFile ] ,
) -> BuildingTypeRecoveredTableSummary {
2026-04-19 15:28:23 -07:00
const RECOVERED_STYLE_THEMES : [ & str ; 6 ] =
[ " Victorian " , " Tudor " , " SoWest " , " Persian " , " Kyoto " , " ClpBrd " ] ;
2026-04-19 14:58:27 -07:00
const RECOVERED_SOURCE_KINDS : [ & str ; 5 ] = [
" StationSml " ,
" StationMed " ,
" StationLrg " ,
" ServiceTower " ,
" Maintenance " ,
] ;
let entry_by_canonical = entries
. iter ( )
. map ( | entry | ( entry . canonical_stem . clone ( ) , entry ) )
. collect ::< BTreeMap < _ , _ > > ( ) ;
let mut present_style_station_entries = Vec ::new ( ) ;
for style in RECOVERED_STYLE_THEMES {
for source_kind in [ " StationSml " , " StationMed " , " StationLrg " ] {
let canonical = canonicalize_building_stem ( & format! ( " {style} {source_kind} " ) ) ;
if let Some ( entry ) = entry_by_canonical . get ( & canonical ) {
if let Some ( raw_stem ) = entry . raw_stems . first ( ) {
present_style_station_entries . push ( raw_stem . clone ( ) ) ;
}
}
}
}
present_style_station_entries . sort ( ) ;
present_style_station_entries . dedup ( ) ;
let mut present_standalone_entries = Vec ::new ( ) ;
for raw_name in [ " ServiceTower " , " Maintenance " ] {
let canonical = canonicalize_building_stem ( raw_name ) ;
if let Some ( entry ) = entry_by_canonical . get ( & canonical ) {
if let Some ( raw_stem ) = entry . raw_stems . first ( ) {
present_standalone_entries . push ( raw_stem . clone ( ) ) ;
}
}
}
present_standalone_entries . sort ( ) ;
present_standalone_entries . dedup ( ) ;
let mut bare_port_warehouse_files = files
. iter ( )
2026-04-19 15:28:23 -07:00
. filter ( | file | matches! ( file . canonical_stem . as_str ( ) , " port " | " warehouse " ) )
2026-04-19 14:58:27 -07:00
. map ( | file | file . file_name . clone ( ) )
. collect ::< Vec < _ > > ( ) ;
bare_port_warehouse_files . sort ( ) ;
bare_port_warehouse_files . dedup ( ) ;
2026-04-19 15:09:09 -07:00
let mut nonzero_bty_header_dword_groups = BTreeMap ::< u32 , Vec < String > > ::new ( ) ;
for file in files {
let Some ( probe ) = & file . bty_header_probe else {
continue ;
} ;
if probe . dword_0xbb = = 0 {
continue ;
}
nonzero_bty_header_dword_groups
. entry ( probe . dword_0xbb )
. or_default ( )
. push ( file . file_name . clone ( ) ) ;
}
let nonzero_bty_header_dword_summaries = nonzero_bty_header_dword_groups
. into_iter ( )
. map ( | ( dword_0xbb , mut file_names ) | {
file_names . sort ( ) ;
file_names . dedup ( ) ;
BuildingTypeBtyHeaderDwordSummary {
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 ( ) ;
2026-04-19 15:28:23 -07:00
let nonzero_bty_header_name_0x40_summaries =
summarize_nonzero_bty_header_name_lane ( files , 0x40 , | probe | & probe . name_0x40 ) ;
let nonzero_bty_header_name_0x5e_summaries =
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 ) ;
2026-04-19 14:58:27 -07:00
BuildingTypeRecoveredTableSummary {
recovered_style_themes : RECOVERED_STYLE_THEMES
. into_iter ( )
. map ( str ::to_string )
. collect ( ) ,
recovered_source_kinds : RECOVERED_SOURCE_KINDS
. into_iter ( )
. map ( str ::to_string )
. collect ( ) ,
present_style_station_entries ,
present_standalone_entries ,
bare_port_warehouse_files ,
2026-04-19 15:09:09 -07:00
nonzero_bty_header_dword_summaries ,
2026-04-19 15:28:23 -07:00
nonzero_bty_header_name_0x40_summaries ,
nonzero_bty_header_name_0x5e_summaries ,
nonzero_bty_header_name_0x7c_summaries ,
2026-04-19 14:58:27 -07:00
}
}
2026-04-19 15:28:23 -07:00
fn summarize_nonzero_bty_header_name_lane (
files : & [ BuildingTypeSourceFile ] ,
offset : u32 ,
selector : impl Fn ( & BuildingTypeBtyHeaderProbe ) -> & String ,
) -> Vec < BuildingTypeBtyHeaderNameSummary > {
let mut groups = BTreeMap ::< String , Vec < String > > ::new ( ) ;
for file in files {
let Some ( probe ) = & file . bty_header_probe else {
continue ;
} ;
if probe . dword_0xbb = = 0 {
continue ;
}
let header_value = selector ( probe ) . trim ( ) ;
if header_value . is_empty ( ) {
continue ;
}
groups
. entry ( header_value . to_string ( ) )
. or_default ( )
. push ( file . file_name . clone ( ) ) ;
}
let mut summaries = groups
. into_iter ( )
. map ( | ( header_value , mut file_names ) | {
file_names . sort ( ) ;
file_names . dedup ( ) ;
BuildingTypeBtyHeaderNameSummary {
header_offset_hex : format ! ( " 0x{offset:02x} " ) ,
header_value ,
file_count : file_names . len ( ) ,
sample_file_names : file_names . into_iter ( ) . take ( 24 ) . collect ( ) ,
}
} )
. collect ::< Vec < _ > > ( ) ;
summaries . sort_by ( | left , right | {
right
. file_count
. cmp ( & left . file_count )
. then_with ( | | left . header_offset_hex . cmp ( & right . header_offset_hex ) )
. then_with ( | | left . header_value . cmp ( & right . header_value ) )
} ) ;
summaries
}
2026-04-19 03:02:11 -07:00
#[ derive(Debug, Clone, PartialEq, Eq, Deserialize) ]
struct BuildingBindingArtifact {
bindings : Vec < BuildingBindingRow > ,
}
#[ derive(Debug, Clone, PartialEq, Eq, Deserialize) ]
struct BuildingBindingRow {
#[ serde(default) ]
candidate_name : Option < String > ,
}
2026-04-19 12:11:58 -07:00
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn probes_bca_selector_bytes_from_fixed_offsets ( ) {
let mut bytes = vec! [ 0 u8 ; 0xbc + 1 ] ;
bytes [ 0xb8 ] = 0x12 ;
bytes [ 0xb9 ] = 0x34 ;
bytes [ 0xba ] = 0x56 ;
bytes [ 0xbb ] = 0x78 ;
let probe = probe_bca_selector_bytes ( & bytes ) ;
assert_eq! ( probe . byte_0xb8 , 0x12 ) ;
assert_eq! ( probe . byte_0xb9 , 0x34 ) ;
assert_eq! ( probe . byte_0xba , 0x56 ) ;
assert_eq! ( probe . byte_0xbb , 0x78 ) ;
assert_eq! ( probe . byte_0xb8_hex , " 0x12 " ) ;
assert_eq! ( probe . byte_0xbb_hex , " 0x78 " ) ;
}
2026-04-19 14:58:27 -07:00
2026-04-19 15:05:48 -07:00
#[ test ]
fn probes_bty_header_from_fixed_offsets ( ) {
let mut bytes = vec! [ 0 u8 ; 0xc0 ] ;
bytes [ 0x00 .. 0x04 ] . copy_from_slice ( & 0x03eb u32 . 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 ( & 0x89abcdef u32 . 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 " ) ;
}
2026-04-19 14:58:27 -07:00
#[ test ]
fn summarizes_recovered_table_families_from_entries_and_files ( ) {
let entries = vec! [
BuildingTypeSourceEntry {
canonical_stem : canonicalize_building_stem ( " VictorianStationSml " ) ,
raw_stems : vec ! [ " VictorianStationSml " . to_string ( ) ] ,
source_kinds : vec ! [ BuildingTypeSourceKind ::Bty ] ,
file_names : vec ! [ " VictorianStationSml.bty " . to_string ( ) ] ,
} ,
BuildingTypeSourceEntry {
canonical_stem : canonicalize_building_stem ( " ClpBrdStationLrg " ) ,
raw_stems : vec ! [ " ClpbrdStationLrg " . to_string ( ) ] ,
source_kinds : vec ! [ BuildingTypeSourceKind ::Bty ] ,
file_names : vec ! [ " ClpbrdStationLrg.bty " . to_string ( ) ] ,
} ,
BuildingTypeSourceEntry {
canonical_stem : canonicalize_building_stem ( " Maintenance " ) ,
raw_stems : vec ! [ " Maintenance " . to_string ( ) ] ,
source_kinds : vec ! [ BuildingTypeSourceKind ::Bty ] ,
file_names : vec ! [ " Maintenance.bty " . to_string ( ) ] ,
} ,
BuildingTypeSourceEntry {
canonical_stem : canonicalize_building_stem ( " ServiceTower " ) ,
raw_stems : vec ! [ " ServiceTower " . to_string ( ) ] ,
source_kinds : vec ! [ BuildingTypeSourceKind ::Bty ] ,
file_names : vec ! [ " ServiceTower.bty " . to_string ( ) ] ,
} ,
] ;
let files = vec! [
BuildingTypeSourceFile {
file_name : " Port.bty " . to_string ( ) ,
raw_stem : " Port " . to_string ( ) ,
canonical_stem : canonicalize_building_stem ( " Port " ) ,
source_kind : BuildingTypeSourceKind ::Bty ,
byte_len : None ,
bca_selector_probe : None ,
2026-04-19 15:09:09 -07:00
bty_header_probe : Some ( BuildingTypeBtyHeaderProbe {
type_id : 0x03ec ,
type_id_hex : " 0x000003ec " . to_string ( ) ,
name_0x04 : " Port " . to_string ( ) ,
name_0x22 : " Port " . to_string ( ) ,
name_0x40 : " Port " . to_string ( ) ,
name_0x5e : " TextileMill " . to_string ( ) ,
name_0x7c : " Port " . to_string ( ) ,
name_0x9a : " Port " . to_string ( ) ,
byte_0xb8 : 0x06 ,
byte_0xb8_hex : " 0x06 " . to_string ( ) ,
byte_0xb9 : 0x06 ,
byte_0xb9_hex : " 0x06 " . to_string ( ) ,
byte_0xba : 0x30 ,
byte_0xba_hex : " 0x30 " . to_string ( ) ,
dword_0xbb : 0x01f4 ,
dword_0xbb_hex : " 0x000001f4 " . to_string ( ) ,
} ) ,
2026-04-19 14:58:27 -07:00
} ,
BuildingTypeSourceFile {
file_name : " Warehouse.bca " . to_string ( ) ,
raw_stem : " Warehouse " . to_string ( ) ,
canonical_stem : canonicalize_building_stem ( " Warehouse " ) ,
source_kind : BuildingTypeSourceKind ::Bca ,
byte_len : None ,
bca_selector_probe : None ,
2026-04-19 15:05:48 -07:00
bty_header_probe : None ,
2026-04-19 14:58:27 -07:00
} ,
] ;
let summary = summarize_recovered_table_families ( & entries , & files ) ;
2026-04-19 15:28:23 -07:00
assert! (
summary
. present_style_station_entries
. contains ( & " VictorianStationSml " . to_string ( ) )
) ;
assert! (
summary
. present_style_station_entries
. contains ( & " ClpbrdStationLrg " . to_string ( ) )
) ;
2026-04-19 14:58:27 -07:00
assert_eq! (
summary . present_standalone_entries ,
vec! [ " Maintenance " . to_string ( ) , " ServiceTower " . to_string ( ) ]
) ;
assert_eq! (
summary . bare_port_warehouse_files ,
vec! [ " Port.bty " . to_string ( ) , " Warehouse.bca " . to_string ( ) ]
) ;
2026-04-19 15:09:09 -07:00
assert_eq! ( summary . nonzero_bty_header_dword_summaries . len ( ) , 1 ) ;
assert_eq! (
summary . nonzero_bty_header_dword_summaries [ 0 ] . dword_0xbb_hex ,
" 0x000001f4 "
) ;
assert_eq! (
summary . nonzero_bty_header_dword_summaries [ 0 ] . sample_file_names ,
vec! [ " Port.bty " . to_string ( ) ]
) ;
2026-04-19 15:28:23 -07:00
assert_eq! (
summary . nonzero_bty_header_name_0x40_summaries ,
vec! [ BuildingTypeBtyHeaderNameSummary {
header_offset_hex : " 0x40 " . to_string ( ) ,
header_value : " Port " . to_string ( ) ,
file_count : 1 ,
sample_file_names : vec ! [ " Port.bty " . to_string ( ) ] ,
} ]
) ;
assert_eq! (
summary . nonzero_bty_header_name_0x5e_summaries ,
vec! [ BuildingTypeBtyHeaderNameSummary {
header_offset_hex : " 0x5e " . to_string ( ) ,
header_value : " TextileMill " . to_string ( ) ,
file_count : 1 ,
sample_file_names : vec ! [ " Port.bty " . to_string ( ) ] ,
} ]
) ;
assert_eq! (
summary . nonzero_bty_header_name_0x7c_summaries ,
vec! [ BuildingTypeBtyHeaderNameSummary {
header_offset_hex : " 0x7c " . to_string ( ) ,
header_value : " Port " . to_string ( ) ,
file_count : 1 ,
sample_file_names : vec ! [ " Port.bty " . to_string ( ) ] ,
} ]
) ;
2026-04-19 14:58:27 -07:00
}
2026-04-19 12:11:58 -07:00
}