Extend event effects through add-building strip

This commit is contained in:
Jan Petykiewicz 2026-04-19 01:46:59 -07:00
commit b2da02befa
7 changed files with 3319 additions and 1161 deletions

View file

@ -125,6 +125,9 @@ enum Command {
RuntimeInspectSmp {
smp_path: PathBuf,
},
RuntimeInspectCompactEventDispatchCluster {
root_path: PathBuf,
},
RuntimeSummarizeSaveLoad {
smp_path: PathBuf,
},
@ -293,6 +296,51 @@ struct RuntimeSmpInspectionOutput {
inspection: SmpInspectionReport,
}
#[derive(Debug, Serialize)]
struct RuntimeCompactEventDispatchClusterOutput {
root_path: String,
report: RuntimeCompactEventDispatchClusterReport,
}
#[derive(Debug, Serialize)]
struct RuntimeCompactEventDispatchClusterReport {
maps_scanned: usize,
maps_with_event_runtime_collection: usize,
maps_with_dispatch_strip_records: usize,
dispatch_strip_record_count: usize,
unknown_descriptor_ids: Vec<u32>,
unknown_descriptor_special_condition_label_matches: Vec<String>,
unknown_descriptor_occurrences:
BTreeMap<u32, Vec<RuntimeCompactEventDispatchClusterOccurrence>>,
}
#[derive(Debug, Clone, Serialize)]
struct RuntimeCompactEventDispatchClusterOccurrence {
path: String,
record_index: usize,
live_entry_id: u32,
payload_family: String,
signature_family: Option<String>,
condition_tuples: Vec<RuntimeCompactEventDispatchClusterConditionTuple>,
rows: Vec<RuntimeCompactEventDispatchClusterRow>,
}
#[derive(Debug, Clone, Serialize)]
struct RuntimeCompactEventDispatchClusterConditionTuple {
raw_condition_id: i32,
subtype: u8,
metric: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
struct RuntimeCompactEventDispatchClusterRow {
group_index: usize,
descriptor_id: u32,
descriptor_label: Option<String>,
opcode: u8,
raw_scalar_value: i32,
}
#[derive(Debug, Serialize)]
struct RuntimeSaveLoadSummaryOutput {
path: String,
@ -898,6 +946,9 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
Command::RuntimeInspectSmp { smp_path } => {
run_runtime_inspect_smp(&smp_path)?;
}
Command::RuntimeInspectCompactEventDispatchCluster { root_path } => {
run_runtime_inspect_compact_event_dispatch_cluster(&root_path)?;
}
Command::RuntimeSummarizeSaveLoad { smp_path } => {
run_runtime_summarize_save_load(&smp_path)?;
}
@ -1116,6 +1167,14 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
smp_path: PathBuf::from(path),
})
}
[command, subcommand, root_path]
if command == "runtime"
&& subcommand == "inspect-compact-event-dispatch-cluster" =>
{
Ok(Command::RuntimeInspectCompactEventDispatchCluster {
root_path: PathBuf::from(root_path),
})
}
[command, subcommand, path]
if command == "runtime" && subcommand == "summarize-save-load" =>
{
@ -1400,7 +1459,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
})
}
_ => Err(
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime inspect-save-company-chairman <file.smp> | runtime inspect-save-placed-structure-triplets <file.smp> | runtime compare-region-fixed-row-runs <left.gms> <right.gms> | runtime inspect-periodic-company-service-trace <file.smp> | runtime inspect-region-service-trace <file.smp> | runtime inspect-infrastructure-asset-trace <file.smp> | runtime inspect-save-region-queued-notice-records <file.smp> | runtime inspect-placed-structure-dynamic-side-buffer <file.smp> | runtime inspect-unclassified-save-collections <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime export-overlay-import <snapshot.json> <save-slice.json> <overlay-import.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-cargo-types <CargoTypes-dir> | runtime inspect-cargo-skins <Cargo106.PK4> | runtime inspect-cargo-economy-sources <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-production-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-price-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime inspect-compact-event-dispatch-cluster <maps-dir> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime inspect-save-company-chairman <file.smp> | runtime inspect-save-placed-structure-triplets <file.smp> | runtime compare-region-fixed-row-runs <left.gms> <right.gms> | runtime inspect-periodic-company-service-trace <file.smp> | runtime inspect-region-service-trace <file.smp> | runtime inspect-infrastructure-asset-trace <file.smp> | runtime inspect-save-region-queued-notice-records <file.smp> | runtime inspect-placed-structure-dynamic-side-buffer <file.smp> | runtime inspect-unclassified-save-collections <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime export-overlay-import <snapshot.json> <save-slice.json> <overlay-import.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-cargo-types <CargoTypes-dir> | runtime inspect-cargo-skins <Cargo106.PK4> | runtime inspect-cargo-economy-sources <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-production-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-price-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
.into(),
),
}
@ -1605,6 +1664,116 @@ fn run_runtime_inspect_smp(smp_path: &Path) -> Result<(), Box<dyn std::error::Er
Ok(())
}
fn run_runtime_inspect_compact_event_dispatch_cluster(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut input_paths = Vec::new();
collect_compact_event_dispatch_cluster_input_paths(root_path, &mut input_paths)?;
input_paths.sort();
let mut maps_with_event_runtime_collection = 0usize;
let mut maps_with_dispatch_strip_records = 0usize;
let mut dispatch_strip_record_count = 0usize;
let mut unknown_descriptor_occurrences =
BTreeMap::<u32, Vec<RuntimeCompactEventDispatchClusterOccurrence>>::new();
for path in &input_paths {
let inspection = inspect_smp_file(path)?;
let Some(summary) = inspection.event_runtime_collection_summary else {
continue;
};
maps_with_event_runtime_collection += 1;
let mut map_dispatch_strip_record_count = 0usize;
for record in &summary.records {
let matching_rows = record
.grouped_effect_rows
.iter()
.filter(|row| {
compact_event_dispatch_strip_opcode(row.opcode)
&& row.descriptor_label.is_none()
})
.fold(
BTreeMap::<u32, Vec<RuntimeCompactEventDispatchClusterRow>>::new(),
|mut grouped, row| {
grouped.entry(row.descriptor_id).or_default().push(
RuntimeCompactEventDispatchClusterRow {
group_index: row.group_index,
descriptor_id: row.descriptor_id,
descriptor_label: row.descriptor_label.clone(),
opcode: row.opcode,
raw_scalar_value: row.raw_scalar_value,
},
);
grouped
},
);
if matching_rows.is_empty() {
continue;
}
map_dispatch_strip_record_count += 1;
let condition_tuples = record
.standalone_condition_rows
.iter()
.map(|row| RuntimeCompactEventDispatchClusterConditionTuple {
raw_condition_id: row.raw_condition_id,
subtype: row.subtype,
metric: row.metric.clone(),
})
.collect::<Vec<_>>();
let signature_family = compact_event_signature_family_from_notes(&record.notes);
for (descriptor_id, rows) in matching_rows {
unknown_descriptor_occurrences
.entry(descriptor_id)
.or_default()
.push(RuntimeCompactEventDispatchClusterOccurrence {
path: path.display().to_string(),
record_index: record.record_index,
live_entry_id: record.live_entry_id,
payload_family: record.payload_family.clone(),
signature_family: signature_family.clone(),
condition_tuples: condition_tuples.clone(),
rows,
});
}
}
if map_dispatch_strip_record_count > 0 {
maps_with_dispatch_strip_records += 1;
dispatch_strip_record_count += map_dispatch_strip_record_count;
}
}
let unknown_descriptor_ids = unknown_descriptor_occurrences
.keys()
.copied()
.collect::<Vec<_>>();
let unknown_descriptor_special_condition_label_matches = unknown_descriptor_ids
.iter()
.filter_map(|descriptor_id| {
special_condition_label_for_compact_dispatch_descriptor(*descriptor_id)
.map(|label| format!("{descriptor_id} -> {label}"))
})
.collect::<Vec<_>>();
let report = RuntimeCompactEventDispatchClusterOutput {
root_path: root_path.display().to_string(),
report: RuntimeCompactEventDispatchClusterReport {
maps_scanned: input_paths.len(),
maps_with_event_runtime_collection,
maps_with_dispatch_strip_records,
dispatch_strip_record_count,
unknown_descriptor_ids,
unknown_descriptor_special_condition_label_matches,
unknown_descriptor_occurrences,
},
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
fn run_runtime_summarize_save_load(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let summary = inspection.save_load_summary.ok_or_else(|| {
@ -3731,6 +3900,73 @@ fn parse_special_condition_slot_index(label: &str) -> Option<u8> {
slot_index.parse().ok()
}
fn compact_event_dispatch_strip_opcode(opcode: u8) -> bool {
matches!(opcode, 0x04..=0x08 | 0x0d | 0x10..=0x13 | 0x16)
}
fn compact_event_signature_family_from_notes(notes: &[String]) -> Option<String> {
notes.iter().find_map(|note| {
note.strip_prefix("compact signature family = ")
.map(ToString::to_string)
})
}
fn special_condition_label_for_compact_dispatch_descriptor(
descriptor_id: u32,
) -> Option<&'static str> {
let band_index = descriptor_id.checked_sub(535)? as usize;
SPECIAL_CONDITION_LABELS.get(band_index).copied()
}
fn collect_compact_event_dispatch_cluster_input_paths(
root_path: &Path,
out: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = match fs::symlink_metadata(root_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
if metadata.file_type().is_symlink() {
return Ok(());
}
if root_path.is_file() {
if root_path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("gmp"))
{
out.push(root_path.to_path_buf());
}
return Ok(());
}
let entries = match fs::read_dir(root_path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_compact_event_dispatch_cluster_input_paths(&path, out)?;
continue;
}
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("gmp"))
{
out.push(path);
}
}
Ok(())
}
fn parse_hex_offset(text: &str) -> Option<usize> {
text.strip_prefix("0x")
.and_then(|digits| usize::from_str_radix(digits, 16).ok())

View file

@ -9485,6 +9485,16 @@ fn parse_event_runtime_collection_summary_with_tag_width(
.collect::<Vec<_>>();
mutation_candidate_unknown_descriptor_ids.sort_unstable();
mutation_candidate_unknown_descriptor_ids.dedup();
let mut mutation_candidate_special_condition_label_matches =
mutation_candidate_unknown_descriptor_ids
.iter()
.filter_map(|descriptor_id| {
known_special_condition_label_for_compact_descriptor_id(*descriptor_id)
.map(|label| format!("{descriptor_id} -> {label}"))
})
.collect::<Vec<_>>();
mutation_candidate_special_condition_label_matches.sort();
mutation_candidate_special_condition_label_matches.dedup();
let mut dispatch_strip_unknown_condition_ids = records
.iter()
.filter(|record| {
@ -9533,6 +9543,12 @@ fn parse_event_runtime_collection_summary_with_tag_width(
mutation_candidate_unknown_descriptor_ids
));
}
if !mutation_candidate_special_condition_label_matches.is_empty() {
control_lane_notes.push(format!(
"unlabeled 0x00431b20 dispatch-strip descriptor ids matching known special-condition label_id-2000 values = {:?}",
mutation_candidate_special_condition_label_matches
));
}
if !dispatch_strip_unknown_condition_ids.is_empty() {
control_lane_notes.push(format!(
"standalone condition ids still missing checked-in labels in the 0x00431b20 dispatch strip = {:?}",
@ -9587,6 +9603,16 @@ fn opcode_reaches_world_apply_compact_runtime_effect_dispatch_strip(opcode: u8)
matches!(opcode, 0x04..=0x08 | 0x0d | 0x10..=0x13 | 0x16)
}
fn known_special_condition_label_for_compact_descriptor_id(
descriptor_id: u32,
) -> Option<&'static str> {
let label_id = descriptor_id.checked_add(2000)?;
KNOWN_SPECIAL_CONDITION_DEFINITIONS
.iter()
.find(|definition| definition.label_id == label_id)
.map(|definition| definition.label)
}
fn try_parse_nondirect_event_runtime_record_summaries(
records_payload: &[u8],
records_payload_offset: usize,
@ -23874,8 +23900,8 @@ mod tests {
#[test]
fn checked_in_event_effect_table_covers_the_full_exported_descriptor_set() {
let rows = checked_in_event_effect_descriptor_rows();
assert_eq!(rows.len(), 520);
for descriptor_id in 0..520_u32 {
assert_eq!(rows.len(), 614);
for descriptor_id in 0..614_u32 {
assert!(
real_grouped_effect_descriptor_metadata(descriptor_id).is_some(),
"descriptor {descriptor_id} should be recoverable from the checked-in effect table"