diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 18ed215..d148c12 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -4781,6 +4781,7 @@ fn build_periodic_company_service_trace_report( "the trigger-kind field itself is now bounded as an ordinary loaded per-event lane rather than a startup-only special class: restore-side loader 0x00433130 repopulates live event collection 0x0062be18 from packed chunk family 0x4e21/0x4e22, and the event-detail editor strip 0x004d90ba..0x004d91ed writes [event+0x7ef] across the full 0x00..0x0a range through controls 0x4e98..0x4ea2, including kind 8 at 0x004d91b3".to_string(), "that keeps 0x00444d92 -> 0x00432f40(kind 8) on the ordinary loaded runtime-effect pipeline too: world bring-up is servicing pre-existing rows from 0x0062be18 rather than a one-off startup-only record class synthesized outside the collection".to_string(), "the event-detail editor family now ties that trigger-kind field to the ordinary runtime-effect builders too: selected-event control family 0x004db02a / 0x004db1b8..0x004db309 mirrors current [event+0x7ef] back into controls 0x4e98..0x4ea2 under root control 0x4e84, while editor-side builder 0x004db9e5..0x004db9f1 allocates a runtime-effect row from compact payload into 0x0062be18 through 0x00432ea0 before rebinding the selected event id".to_string(), + "bundle-side inspection now grounds the ordinary startup collection further too: War Effort.gmp exposes a non-direct 0x4e99/0x4e9a/0x4e9b runtime-event collection at 0x74740c/0x7543f4/0x7554cf with 24 live rows, and those rows now segment cleanly as compact 0x526f-delimited bodies with repeated 0x4eb8 grouped-effect markers plus optional 0x4eb9 terminators rather than disappearing behind a missing bundle probe".to_string(), ], blockers: vec![ "current atlas evidence now grounds one tuple-backed owner path too: loader tuple field [+0x0c] reaches [site+0x276] through 0x0046f073 / 0x004707ff -> 0x0040ef10, but the classified 0x004707ff caller belongs to multiplayer transport selector-0x13 rather than ordinary save-load restore, so a non-transport persisted source family is still needed for shellless acquisition".to_string(), @@ -4788,7 +4789,7 @@ fn build_periodic_company_service_trace_report( "the paired collection-side triplet serializer 0x00413440 is ruled down too, so the missing ordinary restored-row owner seam likely sits outside the currently bounded direct allocator/finalize/store families and the tagged 0x36b1/0x36b2/0x36b3 load-save strip".to_string(), "the load-side stream owner 0x00413280 is ruled down to cached-source/candidate replay through vtable slot +0x40 and 0x0040ce60, so the missing ordinary restored-row owner seam still sits beyond the current stream-load bridge too".to_string(), "the checked ordinary restore ordering is ruled down too: 0x00413280 stream load, 0x00481210 dynamic side-buffer refresh, and 0x004133b0 local-runtime replay all sit on the bring-up strip without re-entering 0x004134d0 / 0x0040f6d0 / 0x0040ef10 for already-restored rows".to_string(), - "the grouped opcode dispatcher 0x00431b20 is still not a tagged restore owner, but the remaining uncertainty is narrower now than 'is kind 8 synthetic' or 'does kind 8 live on a separate editor/build class': restore-side 0x00433130 reloads ordinary live event rows into 0x0062be18, the event-detail editor exposes [event+0x7ef] across 0x00..0x0a including kind 8, and the same editor family reaches ordinary runtime-effect allocator 0x00432ea0, so the open question is which loaded kind-8 rows can actually reach the placed-structure mutation opcodes under 0x00431b20".to_string(), + "the grouped opcode dispatcher 0x00431b20 is still not a tagged restore owner, but the remaining uncertainty is narrower now than 'is kind 8 synthetic' or 'does kind 8 live on a separate editor/build class': restore-side 0x00433130 reloads ordinary live event rows into 0x0062be18, the event-detail editor exposes [event+0x7ef] across 0x00..0x0a including kind 8, the same editor family reaches ordinary runtime-effect allocator 0x00432ea0, and War Effort.gmp now proves that the bundle-side collection carries 24 compact non-direct 0x526f/0x4eb8(/0x4eb9) row bodies, so the open question is the field mapping inside that compact row family and which loaded kind-8 rows can actually reach the placed-structure mutation opcodes under 0x00431b20".to_string(), ], }, SmpServiceConsumerHypothesis { @@ -9265,12 +9266,44 @@ fn parse_event_runtime_collection_summary( container_profile: Option<&SmpContainerProfile>, save_load_summary: Option<&SmpSaveLoadSummary>, ) -> Option { - let metadata_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG); - let record_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG); - let close_offsets = find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG); + parse_event_runtime_collection_summary_with_tag_width( + bytes, + container_profile, + save_load_summary, + 2, + ) + .or_else(|| { + parse_event_runtime_collection_summary_with_tag_width( + bytes, + container_profile, + save_load_summary, + 4, + ) + }) +} + +fn parse_event_runtime_collection_summary_with_tag_width( + bytes: &[u8], + container_profile: Option<&SmpContainerProfile>, + save_load_summary: Option<&SmpSaveLoadSummary>, + tag_width: usize, +) -> Option { + let (metadata_offsets, record_offsets, close_offsets) = match tag_width { + 2 => ( + find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG), + find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG), + find_u16_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG), + ), + 4 => ( + find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32), + find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32), + find_u32_le_offsets(bytes, EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32), + ), + _ => return None, + }; for metadata_tag_offset in metadata_offsets { - let packed_state_version = read_u32_at(bytes, metadata_tag_offset + 2)?; + let packed_state_version = read_u32_at(bytes, metadata_tag_offset + tag_width)?; if packed_state_version != EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION { continue; } @@ -9278,12 +9311,13 @@ fn parse_event_runtime_collection_summary( let records_tag_offset = record_offsets .iter() .copied() - .find(|offset| *offset > metadata_tag_offset + 6)?; + .find(|offset| *offset > metadata_tag_offset + tag_width + 4)?; let close_tag_offset = close_offsets .iter() .copied() .find(|offset| *offset > records_tag_offset)?; - let metadata_payload = bytes.get(metadata_tag_offset + 6..records_tag_offset)?; + let metadata_payload = + bytes.get(metadata_tag_offset + tag_width + 4..records_tag_offset)?; if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { continue; } @@ -9295,34 +9329,62 @@ fn parse_event_runtime_collection_summary( let direct_record_stride = usize::try_from(header_words[1]).ok()?; let live_id_bound = header_words[4]; let live_record_count = usize::try_from(header_words[5]).ok()?; - if direct_collection_flag == 0 || direct_record_stride == 0 { - continue; - } - let bitset_len = ((usize::try_from(live_id_bound).ok()?).saturating_add(15)) / 8; - let payload_bytes = direct_record_stride.checked_mul(live_record_count)?; - if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + bitset_len { - continue; - } - if metadata_payload.len() < bitset_len + payload_bytes { - continue; - } + let records_payload = bytes.get(records_tag_offset + tag_width..close_tag_offset)?; + let (source_kind, live_entry_ids) = if direct_collection_flag == 0 && tag_width == 4 { + ( + "packed-event-runtime-collection-nondirect".to_string(), + (1..=u32::try_from(live_record_count).ok()?).collect::>(), + ) + } else { + if direct_collection_flag == 0 || direct_record_stride == 0 { + continue; + } - let bitset_offset = metadata_payload.len() - bitset_len - payload_bytes; - if bitset_offset < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { - continue; - } - let bitset = metadata_payload.get(bitset_offset..bitset_offset + bitset_len)?; - let live_entry_ids = decode_live_entry_ids_from_tombstone_bitset(bitset, live_id_bound)?; - if live_entry_ids.len() != live_record_count { - continue; - } - let records_payload = bytes.get(records_tag_offset + 2..close_tag_offset)?; - let records = parse_event_runtime_record_summaries( - records_payload, - records_tag_offset + 2, - &live_entry_ids, - ); + let bitset_len = ((usize::try_from(live_id_bound).ok()?).saturating_add(15)) / 8; + let payload_bytes = direct_record_stride.checked_mul(live_record_count)?; + if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN + bitset_len { + continue; + } + if metadata_payload.len() < bitset_len + payload_bytes { + continue; + } + + let bitset_offset = metadata_payload.len() - bitset_len - payload_bytes; + if bitset_offset < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN { + continue; + } + let bitset = metadata_payload.get(bitset_offset..bitset_offset + bitset_len)?; + let live_entry_ids = + decode_live_entry_ids_from_tombstone_bitset(bitset, live_id_bound)?; + if live_entry_ids.len() != live_record_count { + continue; + } + ( + "packed-event-runtime-collection".to_string(), + live_entry_ids, + ) + }; + let records = if source_kind == "packed-event-runtime-collection-nondirect" { + try_parse_nondirect_event_runtime_record_summaries( + records_payload, + records_tag_offset + tag_width, + &live_entry_ids, + ) + .unwrap_or_else(|| { + parse_event_runtime_record_summaries( + records_payload, + records_tag_offset + tag_width, + &live_entry_ids, + ) + }) + } else { + parse_event_runtime_record_summaries( + records_payload, + records_tag_offset + tag_width, + &live_entry_ids, + ) + }; let decoded_record_count = records .iter() .filter(|record| record.decode_status != "unsupported_framing") @@ -9333,7 +9395,7 @@ fn parse_event_runtime_collection_summary( .count(); return Some(SmpLoadedEventRuntimeCollectionSummary { - source_kind: "packed-event-runtime-collection".to_string(), + source_kind, mechanism_family: save_load_summary .map(|summary| summary.mechanism_family.clone()) .unwrap_or_else(|| "unknown".to_string()), @@ -9359,6 +9421,93 @@ fn parse_event_runtime_collection_summary( None } +fn try_parse_nondirect_event_runtime_record_summaries( + records_payload: &[u8], + records_payload_offset: usize, + live_entry_ids: &[u32], +) -> Option> { + let marker_offsets = find_u16_le_offsets(records_payload, PACKED_EVENT_REAL_CONDITION_MARKER); + if marker_offsets.len() != live_entry_ids.len() || marker_offsets.first().copied() != Some(0) { + return None; + } + + let mut record_offsets = marker_offsets; + record_offsets.push(records_payload.len()); + let mut records = Vec::with_capacity(live_entry_ids.len()); + + for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() { + let start = *record_offsets.get(record_index)?; + let end = *record_offsets.get(record_index + 1)?; + let record_body = records_payload.get(start..end)?; + let grouped_marker_relative_offset = + find_u16_le_offsets(record_body, PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER) + .into_iter() + .next(); + let end_marker_relative_offset = + find_u16_le_offsets(record_body, 0x4eb9).into_iter().next(); + let head_signature_words = read_u16_window(record_body, 0, 18); + let post_group_signature_words = grouped_marker_relative_offset + .map(|offset| offset + 2) + .map(|offset| read_u16_window(record_body, offset, 12)) + .unwrap_or_default(); + let ascii_preview_before_grouped_marker = grouped_marker_relative_offset + .and_then(|offset| record_body.get(..offset).map(ascii_preview)); + + let mut notes = vec![ + "decoded from non-direct 0x4e99/0x4e9a/0x4e9b map-bundle row segmentation using 0x526f-delimited slices".to_string(), + format!( + "head signature u16 words = {}", + format_u16_word_signature(&head_signature_words) + ), + ]; + if let Some(offset) = grouped_marker_relative_offset { + notes.push(format!( + "grouped-effect marker 0x4eb8 at relative offset +0x{offset:x}" + )); + if !post_group_signature_words.is_empty() { + notes.push(format!( + "post-group signature u16 words = {}", + format_u16_word_signature(&post_group_signature_words) + )); + } + } + if let Some(offset) = end_marker_relative_offset { + notes.push(format!( + "row terminator marker 0x4eb9 at relative offset +0x{offset:x}" + )); + } + if let Some(preview) = ascii_preview_before_grouped_marker { + notes.push(format!("ascii preview before grouped marker = {preview}")); + } + + records.push(SmpLoadedPackedEventRecordSummary { + record_index, + live_entry_id, + payload_offset: Some(records_payload_offset + start), + payload_len: Some(end.saturating_sub(start)), + decode_status: "compact_nondirect_parity_only".to_string(), + payload_family: "real_packed_nondirect_compact_v1".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, + standalone_condition_rows: Vec::new(), + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![0, 0, 0, 0], + grouped_effect_rows: Vec::new(), + decoded_conditions: Vec::new(), + decoded_actions: Vec::new(), + executable_import_ready: false, + notes, + }); + } + + Some(records) +} + fn decode_live_entry_ids_from_tombstone_bitset( bitset: &[u8], live_id_bound: u32, @@ -12777,6 +12926,12 @@ fn classify_secondary_variant_probe( evidence.push("third/fourth words 0x21000001 and 0xa0000100".to_string()); "rt3-105-gms-scenario-family-v1".to_string() } + [0x00140000, 0x93e00100, 0x00000004, 0xa0000000, ..] => { + evidence.push("leading word 0x00140000".to_string()); + evidence.push("anchor word 0x93e00100".to_string()); + evidence.push("third/fourth words 0x00000004 and 0xa0000000".to_string()); + "rt3-map-secondary-family-v1".to_string() + } [0x00010000, 0x49f00100, 0x00000002, 0xa0000000, ..] => { evidence.push("leading word 0x00010000".to_string()); evidence.push("anchor word 0x49f00100".to_string()); @@ -12912,6 +13067,15 @@ fn classify_container_profile( ], true, ), + ("gmp", "rt3-map-header-family", "rt3-map-secondary-family-v1") => ( + "rt3-map-container-family".to_string(), + vec![ + "extension .gmp".to_string(), + "map header family".to_string(), + "observed map secondary window family".to_string(), + ], + true, + ), (_, header_family, secondary_family) => ( "unknown".to_string(), vec![ @@ -19980,6 +20144,23 @@ fn read_u32_window(bytes: &[u8], offset: usize, count: usize) -> Vec { words } +fn read_u16_window(bytes: &[u8], offset: usize, count: usize) -> Vec { + let mut words = Vec::new(); + let end = bytes.len().min(offset + count * 2); + for chunk in bytes[offset..end].chunks_exact(2) { + words.push(u16::from_le_bytes([chunk[0], chunk[1]])); + } + words +} + +fn format_u16_word_signature(words: &[u16]) -> String { + words + .iter() + .map(|word| format!("0x{word:04x}")) + .collect::>() + .join(", ") +} + fn read_u8_at(bytes: &[u8], offset: usize) -> Option { bytes.get(offset).copied() } @@ -21421,6 +21602,79 @@ mod tests { assert_eq!(summary.records[0].decode_status, "unsupported_framing"); } + #[test] + fn parses_event_runtime_collection_summary_from_u32_tag_chunks() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + + let header_words = [1u32, 4, 0, 0, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + + bytes.extend_from_slice(&[0x14, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]); + bytes.extend_from_slice(&[0x55, 0x66, 0x77, 0x88]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse from u32 tags"); + + assert_eq!(summary.packed_state_version, 0x3e9); + assert_eq!(summary.live_id_bound, 5); + assert_eq!(summary.live_record_count, 3); + assert_eq!(summary.live_entry_ids, vec![1, 3, 5]); + assert_eq!(summary.records_tag_offset, 98); + assert_eq!(summary.decoded_record_count, 0); + assert_eq!(summary.records.len(), 3); + assert_eq!(summary.records[0].decode_status, "unsupported_framing"); + } + + #[test] + fn parses_nondirect_event_runtime_collection_summary_from_u32_tag_chunks() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + + let header_words = [ + 0u32, 6, 10, 20, 30, 3, 0, 1, 0, 0, 0, 0, 0, 1, 1, 23, 0, 0, 0, + ]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + + bytes.extend_from_slice(&[0u8; 18]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_RECORDS_TAG as u32).to_le_bytes()); + bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + bytes.extend_from_slice(&(EVENT_RUNTIME_COLLECTION_CLOSE_TAG as u32).to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("non-direct event runtime collection summary should parse"); + + assert_eq!( + summary.source_kind, + "packed-event-runtime-collection-nondirect" + ); + assert_eq!(summary.packed_state_version, 0x3e9); + assert_eq!(summary.live_id_bound, 30); + assert_eq!(summary.live_record_count, 3); + assert_eq!(summary.live_entry_ids, vec![1, 2, 3]); + assert_eq!(summary.records_tag_offset, 102); + assert_eq!(summary.decoded_record_count, 0); + assert_eq!(summary.records.len(), 3); + assert_eq!(summary.records[0].decode_status, "unsupported_framing"); + } + fn encode_len_prefixed_string(text: &str) -> Vec { let mut bytes = Vec::with_capacity(1 + text.len()); bytes.push(text.len() as u8); @@ -27751,6 +28005,29 @@ mod tests { assert!(scenario_profile.is_known_profile); assert_eq!(alt_profile.profile_family, "rt3-105-alt-map-container-v1"); assert!(alt_profile.is_known_profile); + + let generic_map_profile = classify_container_profile( + Some("gmp"), + Some(&SmpHeaderVariantProbe { + variant_family: "rt3-map-header-family".to_string(), + variant_evidence: vec![], + is_known_family: true, + }), + Some(&SmpSecondaryVariantProbe { + aligned_window_offset: 0, + words: vec![0x00140000, 0x93e00100, 0x00000004, 0xa0000000], + hex_words: vec![], + variant_family: "rt3-map-secondary-family-v1".to_string(), + variant_evidence: vec![], + }), + ) + .expect("generic map profile"); + + assert_eq!( + generic_map_profile.profile_family, + "rt3-map-container-family" + ); + assert!(generic_map_profile.is_known_profile); } fn empty_analysis_report() -> SmpSaveCompanyChairmanAnalysisReport { diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index 1a3af49..818d2f1 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -241,6 +241,15 @@ Working rule: into `0x0062be18` through `0x00432ea0`; so the remaining startup compact-effect question is no longer whether kind `8` lives on a separate editor/build class either, but which loaded kind-`8` rows actually carry the mutation-capable compact payloads + - bundle-side inspection now grounds the startup collection itself: + `War Effort.gmp` exposes a non-direct `0x4e99/0x4e9a/0x4e9b` runtime-event collection at + `0x74740c / 0x7543f4 / 0x7554cf` with `24` live rows, and those rows now segment cleanly as + compact `0x526f`-delimited bodies with repeated `0x4eb8` grouped-effect markers plus optional + `0x4eb9` terminators + - that moves the startup compact-effect blocker again: + the remaining question is no longer collection existence, but field mapping inside that + compact non-direct row family and whether its observed signatures correspond to loaded + `kind 8` rows that can reach the placed-structure mutation opcodes under `0x00431b20` - the `[site+0x27a]` companion lane is grounded now too: it is a live signed scalar accumulator rather than a second owner-identity seam, with zero-init at `0x0042125d` and `0x0040f793`, accumulation at `0x0040dfec` and `0x00426ad8`, direct set on