22172 lines
911 KiB
Rust
22172 lines
911 KiB
Rust
use std::collections::{BTreeMap, BTreeSet};
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::sync::OnceLock;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use sha2::{Digest, Sha256};
|
|
|
|
use crate::{
|
|
RuntimeCargoClass, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget,
|
|
RuntimeChairmanMetric, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope,
|
|
RuntimeCompanyControllerKind, RuntimeCompanyMarketState, RuntimeCompanyMetric,
|
|
RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeEffect,
|
|
RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimePlayerTarget,
|
|
RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts,
|
|
};
|
|
|
|
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
|
|
const PREAMBLE_U32_WORD_COUNT: usize = 16;
|
|
const MIN_ASCII_RUN_LEN: usize = 8;
|
|
const ASCII_PREVIEW_CHAR_LIMIT: usize = 160;
|
|
const TAG_OFFSET_SAMPLE_LIMIT: usize = 8;
|
|
const EARLY_ZERO_RUN_THRESHOLD: usize = 16;
|
|
const EARLY_PREVIEW_BYTE_LIMIT: usize = 32;
|
|
const EARLY_ALIGNED_WORD_WINDOW_COUNT: usize = 8;
|
|
const SPECIAL_CONDITIONS_OFFSET: usize = 0x0d64;
|
|
const SPECIAL_CONDITION_COUNT: usize = 36;
|
|
const SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT: usize = 35;
|
|
const SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT: usize = 50;
|
|
const SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT: usize = 49;
|
|
const SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET: usize = 0x4a7f;
|
|
const SMP_ALIGNED_RUNTIME_RULE_END_OFFSET: usize =
|
|
SPECIAL_CONDITIONS_OFFSET + SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT * 4;
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_OFFSET: usize = 0x0df4;
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET: usize = 0x0f30;
|
|
const POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET: usize = 0x0f58;
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET: usize =
|
|
SMP_ALIGNED_RUNTIME_RULE_END_OFFSET;
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET: usize = SMP_ALIGNED_RUNTIME_RULE_END_OFFSET;
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET: usize =
|
|
SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET
|
|
+ (POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET - SPECIAL_CONDITIONS_OFFSET);
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET: usize =
|
|
SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET
|
|
+ (POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - SPECIAL_CONDITIONS_OFFSET);
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET: usize = 0x4b47;
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN: usize = 0x12c;
|
|
const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET: usize =
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET
|
|
+ POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN;
|
|
const POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET: usize = 0x0f59;
|
|
const POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET: usize = 0x0f75;
|
|
const POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET: usize = 0x4c74;
|
|
const POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET: usize = 0x4c78;
|
|
const POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET: usize = 0x4c7c;
|
|
const POST_TEXT_FIELD_3_RUNTIME_OBJECT_OFFSET: usize = 0x4c80;
|
|
const POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET: usize = 0x4c88;
|
|
const POST_TEXT_FIELD_5_RUNTIME_OBJECT_OFFSET: usize = 0x4c8c;
|
|
const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET: usize = 0x4c80;
|
|
const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET: usize = 0x4c8c;
|
|
const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET: usize =
|
|
SPECIAL_CONDITIONS_OFFSET
|
|
+ (POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET
|
|
- SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET);
|
|
const POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET: usize =
|
|
SPECIAL_CONDITIONS_OFFSET
|
|
+ (POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET
|
|
- SMP_ALIGNED_RUNTIME_RULE_RUNTIME_OBJECT_OFFSET);
|
|
const LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET: usize = 0x0f78;
|
|
const LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET: usize = 0x0fa7;
|
|
const LOCOMOTIVE_POLICY_FIELD_NEG3_RUNTIME_OBJECT_OFFSET: usize = 0x4ca2;
|
|
const LOCOMOTIVE_POLICY_FIELD_NEG2_RUNTIME_OBJECT_OFFSET: usize = 0x4cae;
|
|
const LOCOMOTIVE_POLICY_FIELD_NEG1_RUNTIME_OBJECT_OFFSET: usize = 0x4cb2;
|
|
const LOCOMOTIVE_POLICY_FIELD_0_RUNTIME_OBJECT_OFFSET: usize = 0x4c93;
|
|
const LOCOMOTIVE_POLICY_FIELD_1_RUNTIME_OBJECT_OFFSET: usize = 0x4c97;
|
|
const LOCOMOTIVE_POLICY_FIELD_2_RUNTIME_OBJECT_OFFSET: usize = 0x4c98;
|
|
const LOCOMOTIVE_POLICY_FIELD_3_RUNTIME_OBJECT_OFFSET: usize = 0x4c99;
|
|
const LOCOMOTIVE_POLICY_FIELD_4_RUNTIME_OBJECT_OFFSET: usize = 0x4cba;
|
|
const LOCOMOTIVE_POLICY_FIELD_5_RUNTIME_OBJECT_OFFSET: usize = 0x4cbe;
|
|
const PRE_RECIPE_SCALAR_PLATEAU_OFFSET: usize = 0x0fa7;
|
|
const PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET: usize = 0x0fe7;
|
|
const RECIPE_BOOK_ROOT_OFFSET: usize = 0x0fe7;
|
|
const RECIPE_BOOK_COUNT: usize = 12;
|
|
const RECIPE_BOOK_STRIDE: usize = 0x4e1;
|
|
const RECIPE_BOOK_HEAD_SAMPLE_LEN: usize = 16;
|
|
const RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET: usize = 0x3ed;
|
|
const RECIPE_BOOK_LINE_AREA_OFFSET: usize = 0x3f1;
|
|
const RECIPE_BOOK_LINE_COUNT: usize = 5;
|
|
const RECIPE_BOOK_LINE_STRIDE: usize = 0x30;
|
|
const RECIPE_BOOK_LINE_AREA_LEN: usize = RECIPE_BOOK_LINE_COUNT * RECIPE_BOOK_LINE_STRIDE;
|
|
const RECIPE_BOOK_SUMMARY_END_OFFSET: usize =
|
|
RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_COUNT * RECIPE_BOOK_STRIDE;
|
|
const RT3_SAVE_WORLD_BLOCK_CHUNK_TAG: u32 = 0x000032c8;
|
|
const RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG: u32 = 0x000032c9;
|
|
const RT3_SAVE_WORLD_BLOCK_LEN: usize = 0x4f2c;
|
|
const RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET: usize = 0x1d;
|
|
const RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET: usize = 0x21;
|
|
const RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET: usize = 0x25;
|
|
const RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET: usize = 0x29;
|
|
const RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET: usize = 0x0d;
|
|
const RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET: usize = 0x11;
|
|
const RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET: usize = 0x15;
|
|
const RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET: usize = 0x19;
|
|
const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_CANDIDATE_FIELDS: [(&str, usize); 11] = [
|
|
(
|
|
"current_calendar_tuple_word",
|
|
RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET,
|
|
),
|
|
(
|
|
"current_calendar_tuple_word_2",
|
|
RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET,
|
|
),
|
|
(
|
|
"absolute_calendar_counter",
|
|
RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET,
|
|
),
|
|
(
|
|
"absolute_calendar_counter_mirror",
|
|
RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET,
|
|
),
|
|
("selection_context_candidate_0", 0x1d),
|
|
("selection_context_candidate_1", 0x21),
|
|
("issue_0x37_multiplier", 0x25),
|
|
("issue_0x37_value", 0x29),
|
|
("issue_neighbor_candidate_0", 0x2d),
|
|
("issue_neighbor_candidate_1", 0x31),
|
|
("issue_neighbor_candidate_2", 0x35),
|
|
];
|
|
const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET: usize = 0x0d;
|
|
const RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS: usize = 17;
|
|
const RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_BASE_TERMS_OFFSET: usize = 0x8a;
|
|
const RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT: usize = 0x3b;
|
|
const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET: usize = 0x83;
|
|
const RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET: usize = 0xc1;
|
|
const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET: usize = 0x0bbf;
|
|
const RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET: usize = 0x4a83;
|
|
const RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET: usize = 0x4a87;
|
|
const RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET: usize = 0x4a8b;
|
|
const RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET: usize = 0x4a8f;
|
|
const RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET: usize = 0x4c78;
|
|
const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET: usize = 0x0bda;
|
|
const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS: [usize; 6] =
|
|
[0x0bde, 0x0be2, 0x0be6, 0x0bea, 0x0bee, 0x0bf2];
|
|
const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT: usize = 16;
|
|
const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE: usize = 9;
|
|
const EVENT_RUNTIME_COLLECTION_METADATA_TAG: u16 = 0x4e99;
|
|
const EVENT_RUNTIME_COLLECTION_RECORDS_TAG: u16 = 0x4e9a;
|
|
const EVENT_RUNTIME_COLLECTION_CLOSE_TAG: u16 = 0x4e9b;
|
|
const EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION: u32 = 0x000003e9;
|
|
const INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT: usize = 19;
|
|
const INDEXED_COLLECTION_SERIALIZED_HEADER_LEN: usize =
|
|
INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT * 4;
|
|
const SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX: usize = 16;
|
|
const SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT: usize = 3;
|
|
const SAVE_REGION_RECORD_NAME_TAG: u16 = 0x55f1;
|
|
const SAVE_REGION_RECORD_POLICY_TAG: u16 = 0x55f2;
|
|
const SAVE_REGION_RECORD_PROFILE_TAG: u16 = 0x55f3;
|
|
const SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED: u32 = 0x005c87a8;
|
|
const SAVE_REGION_QUEUED_NOTICE_NODE_KIND: u32 = 7;
|
|
const SAVE_REGION_QUEUED_NOTICE_NODE_LEN: usize = 0x20;
|
|
const PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC: &[u8; 8] = b"RPEVT001";
|
|
const PACKED_EVENT_RECORD_SYNTHETIC_MAGIC: &[u8; 4] = b"RPE1";
|
|
const PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC: &[u8; 4] = b"RPT1";
|
|
const PACKED_EVENT_REAL_CONDITION_MARKER: u16 = 0x526f;
|
|
const PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER: u16 = 0x4eb8;
|
|
const PACKED_EVENT_REAL_CONDITION_ROW_LEN: usize = 0x1e;
|
|
const PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN: usize = 0x28;
|
|
const PACKED_EVENT_REAL_GROUP_COUNT: usize = 4;
|
|
const PACKED_EVENT_REAL_COMPACT_CONTROL_LEN: usize = 37;
|
|
const PACKED_EVENT_TEXT_BAND_LABELS: [&str; 6] = [
|
|
"primary_text_band",
|
|
"secondary_text_band_0",
|
|
"secondary_text_band_1",
|
|
"secondary_text_band_2",
|
|
"secondary_text_band_3",
|
|
"secondary_text_band_4",
|
|
];
|
|
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX: usize =
|
|
(POST_SPECIAL_CONDITIONS_SCALAR_OFFSET - SPECIAL_CONDITIONS_OFFSET) / 4;
|
|
const SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT: usize =
|
|
(SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET) / 4;
|
|
const SHARED_SIGNATURE_WORDS_1_TO_7: [u32; 7] = [
|
|
0x00002ee0, 0x00040001, 0x00028000, 0x00010000, 0x00000771, 0x00000771, 0x00000771,
|
|
];
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
struct RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: u32,
|
|
label: &'static str,
|
|
target_mask_bits: u8,
|
|
parameter_family: &'static str,
|
|
runtime_key: Option<&'static str>,
|
|
runtime_status: RealGroupedEffectRuntimeStatus,
|
|
executable_in_runtime: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RealGroupedEffectRuntimeStatus {
|
|
Executable,
|
|
ShellOwned,
|
|
EvidenceBlocked,
|
|
VariantOrScopeBlocked,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
struct CheckedInEventEffectsSemanticCatalogArtifact {
|
|
descriptors: Vec<CheckedInEventEffectSemanticRow>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
struct CheckedInEventEffectSemanticRow {
|
|
descriptor_id: u32,
|
|
label: String,
|
|
target_mask_bits: u8,
|
|
parameter_family: String,
|
|
runtime_key: Option<String>,
|
|
runtime_status: String,
|
|
executable_in_runtime: bool,
|
|
}
|
|
|
|
const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 12] = [
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 1,
|
|
label: "Player Cash",
|
|
target_mask_bits: 0x02,
|
|
parameter_family: "player_finance_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 2,
|
|
label: "Company Cash",
|
|
target_mask_bits: 0x01,
|
|
parameter_family: "company_finance_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 3,
|
|
label: "Territory - Allow All",
|
|
target_mask_bits: 0x05,
|
|
parameter_family: "territory_access_toggle",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 8,
|
|
label: "Economic Status",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "whole_game_state_enum",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 108,
|
|
label: "Use Wartime Cargos",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "special_condition_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 109,
|
|
label: "Turbo Diesel Availability",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "candidate_availability_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 110,
|
|
label: "Disable Stock Buying and Selling",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "world_flag_toggle",
|
|
runtime_key: Some("world.disable_stock_buying_and_selling"),
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 9,
|
|
label: "Confiscate All",
|
|
target_mask_bits: 0x01,
|
|
parameter_family: "company_confiscation_variant",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 13,
|
|
label: "Deactivate Company",
|
|
target_mask_bits: 0x01,
|
|
parameter_family: "company_lifecycle_toggle",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 14,
|
|
label: "Deactivate Player",
|
|
target_mask_bits: 0x02,
|
|
parameter_family: "player_lifecycle_toggle",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 15,
|
|
label: "Retire Train",
|
|
target_mask_bits: 0x0d,
|
|
parameter_family: "company_or_territory_asset_toggle",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: 16,
|
|
label: "Company Track Pieces Buildable",
|
|
target_mask_bits: 0x01,
|
|
parameter_family: "company_build_limit_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
},
|
|
];
|
|
|
|
fn real_grouped_effect_runtime_status_name(status: RealGroupedEffectRuntimeStatus) -> &'static str {
|
|
match status {
|
|
RealGroupedEffectRuntimeStatus::Executable => "executable",
|
|
RealGroupedEffectRuntimeStatus::ShellOwned => "shell_owned",
|
|
RealGroupedEffectRuntimeStatus::EvidenceBlocked => "evidence_blocked",
|
|
RealGroupedEffectRuntimeStatus::VariantOrScopeBlocked => "variant_or_scope_blocked",
|
|
}
|
|
}
|
|
|
|
fn checked_in_event_effect_descriptor_rows()
|
|
-> &'static BTreeMap<u32, RealGroupedEffectDescriptorMetadata> {
|
|
static ROWS: OnceLock<BTreeMap<u32, RealGroupedEffectDescriptorMetadata>> = OnceLock::new();
|
|
ROWS.get_or_init(|| {
|
|
let artifact: CheckedInEventEffectsSemanticCatalogArtifact = serde_json::from_str(
|
|
include_str!("../../../artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json"),
|
|
)
|
|
.expect("checked-in event-effects semantic catalog should parse");
|
|
artifact
|
|
.descriptors
|
|
.into_iter()
|
|
.map(|row| {
|
|
(
|
|
row.descriptor_id,
|
|
checked_in_event_effect_descriptor_metadata(row),
|
|
)
|
|
})
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
fn checked_in_event_effect_descriptor_metadata(
|
|
row: CheckedInEventEffectSemanticRow,
|
|
) -> RealGroupedEffectDescriptorMetadata {
|
|
let label = Box::leak(row.label.clone().into_boxed_str()) as &'static str;
|
|
let parameter_family = Box::leak(row.parameter_family.into_boxed_str()) as &'static str;
|
|
let runtime_key = row
|
|
.runtime_key
|
|
.map(|key| Box::leak(key.into_boxed_str()) as &'static str);
|
|
let runtime_status = match row.runtime_status.as_str() {
|
|
"executable" => RealGroupedEffectRuntimeStatus::Executable,
|
|
"shell_owned" => RealGroupedEffectRuntimeStatus::ShellOwned,
|
|
"evidence_blocked" => RealGroupedEffectRuntimeStatus::EvidenceBlocked,
|
|
"variant_or_scope_blocked" => RealGroupedEffectRuntimeStatus::VariantOrScopeBlocked,
|
|
other => panic!("unknown checked-in event-effect runtime status {other}"),
|
|
};
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id: row.descriptor_id,
|
|
label,
|
|
target_mask_bits: row.target_mask_bits,
|
|
parameter_family,
|
|
runtime_key,
|
|
runtime_status,
|
|
executable_in_runtime: row.executable_in_runtime,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn grouped_effect_descriptor_runtime_status_name(
|
|
descriptor_id: u32,
|
|
) -> Option<&'static str> {
|
|
real_grouped_effect_descriptor_metadata(descriptor_id)
|
|
.map(|metadata| real_grouped_effect_runtime_status_name(metadata.runtime_status))
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RealOrdinaryConditionMetric {
|
|
WorldVariable(u32),
|
|
Company(RuntimeCompanyMetric),
|
|
CompanyVariable(u32),
|
|
PlayerVariable(u32),
|
|
Chairman(RuntimeChairmanMetric),
|
|
Territory(RuntimeTerritoryMetric),
|
|
TerritoryVariable(u32),
|
|
CompanyTerritory(RuntimeTrackMetric),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RealWorldConditionKind {
|
|
SpecialCondition { label: &'static str },
|
|
CandidateAvailability,
|
|
NamedLocomotiveAvailability,
|
|
NamedLocomotiveCost,
|
|
CargoProductionSlot,
|
|
CargoProductionTotal,
|
|
FactoryProductionTotal,
|
|
FarmMineProductionTotal,
|
|
OtherCargoProductionTotal,
|
|
LimitedTrackBuildingAmount,
|
|
TerritoryAccessCost,
|
|
EconomicStatus,
|
|
WorldFlag { key: &'static str },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RealOrdinaryConditionKind {
|
|
Numeric(RealOrdinaryConditionMetric),
|
|
WorldState(RealWorldConditionKind),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
struct RealOrdinaryConditionMetadata {
|
|
raw_condition_id: i32,
|
|
label: &'static str,
|
|
kind: RealOrdinaryConditionKind,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
struct KnownCargoSlotDefinition {
|
|
slot_id: u32,
|
|
label: &'static str,
|
|
cargo_class: RuntimeCargoClass,
|
|
descriptor_id: u32,
|
|
}
|
|
|
|
const KNOWN_CARGO_SLOT_DEFINITIONS: [KnownCargoSlotDefinition; 11] = [
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 1,
|
|
label: "Cargo Production Slot 1",
|
|
cargo_class: RuntimeCargoClass::Factory,
|
|
descriptor_id: 230,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 2,
|
|
label: "Cargo Production Slot 2",
|
|
cargo_class: RuntimeCargoClass::Factory,
|
|
descriptor_id: 231,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 3,
|
|
label: "Cargo Production Slot 3",
|
|
cargo_class: RuntimeCargoClass::Factory,
|
|
descriptor_id: 232,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 4,
|
|
label: "Cargo Production Slot 4",
|
|
cargo_class: RuntimeCargoClass::Factory,
|
|
descriptor_id: 233,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 5,
|
|
label: "Cargo Production Slot 5",
|
|
cargo_class: RuntimeCargoClass::FarmMine,
|
|
descriptor_id: 234,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 6,
|
|
label: "Cargo Production Slot 6",
|
|
cargo_class: RuntimeCargoClass::FarmMine,
|
|
descriptor_id: 235,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 7,
|
|
label: "Cargo Production Slot 7",
|
|
cargo_class: RuntimeCargoClass::FarmMine,
|
|
descriptor_id: 236,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 8,
|
|
label: "Cargo Production Slot 8",
|
|
cargo_class: RuntimeCargoClass::FarmMine,
|
|
descriptor_id: 237,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 9,
|
|
label: "Cargo Production Slot 9",
|
|
cargo_class: RuntimeCargoClass::Other,
|
|
descriptor_id: 238,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 10,
|
|
label: "Cargo Production Slot 10",
|
|
cargo_class: RuntimeCargoClass::Other,
|
|
descriptor_id: 239,
|
|
},
|
|
KnownCargoSlotDefinition {
|
|
slot_id: 11,
|
|
label: "Cargo Production Slot 11",
|
|
cargo_class: RuntimeCargoClass::Other,
|
|
descriptor_id: 240,
|
|
},
|
|
];
|
|
|
|
const GROUNDED_LOCOMOTIVE_PREFIX: [&str; 61] = [
|
|
"2-D-2",
|
|
"E-88",
|
|
"Adler 2-2-2",
|
|
"USA 103",
|
|
"American 4-4-0",
|
|
"Atlantic 4-4-2",
|
|
"Baldwin 0-6-0",
|
|
"Be 5/7",
|
|
"Beuth 2-2-2",
|
|
"Big Boy 4-8-8-4",
|
|
"C55 Deltic",
|
|
"Camelback 0-6-0",
|
|
"Challenger 4-6-6-4",
|
|
"Class 01 4-6-2",
|
|
"Class 103",
|
|
"Class 132",
|
|
"Class 500 4-6-0",
|
|
"Class 9100",
|
|
"Class EF 66",
|
|
"Class 6E",
|
|
"Consolidation 2-8-0",
|
|
"Crampton 4-2-0",
|
|
"DD 080-X",
|
|
"DD40AX",
|
|
"Duke Class 4-4-0",
|
|
"E18",
|
|
"E428",
|
|
"Brenner E412",
|
|
"E60CP",
|
|
"Eight Wheeler 4-4-0",
|
|
"EP-2 Bipolar",
|
|
"ET22",
|
|
"F3",
|
|
"Fairlie 0-6-6-0",
|
|
"Firefly 2-2-2",
|
|
"FP45",
|
|
"Ge 6/6 Crocodile",
|
|
"GG1",
|
|
"GP7",
|
|
"H10 2-8-2",
|
|
"HST 125",
|
|
"Kriegslok 2-10-0",
|
|
"Mallard 4-6-2",
|
|
"Norris 4-2-0",
|
|
"Northern 4-8-4",
|
|
"Orca NX462",
|
|
"Pacific 4-6-2",
|
|
"Planet 2-2-0",
|
|
"Re 6/6",
|
|
"Red Devil 4-8-4",
|
|
"S3 4-4-0",
|
|
"NA-90D",
|
|
"Shay (2-Truck)",
|
|
"Shinkansen Series 0",
|
|
"Stirling 4-2-2",
|
|
"Trans-Euro",
|
|
"V200",
|
|
"VL80T",
|
|
"GP 35",
|
|
"U1",
|
|
"Zephyr",
|
|
];
|
|
|
|
const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435;
|
|
const REAL_WORLD_VARIABLE_1_CONDITION_ID: i32 = 2241;
|
|
const REAL_WORLD_VARIABLE_2_CONDITION_ID: i32 = 2242;
|
|
const REAL_WORLD_VARIABLE_3_CONDITION_ID: i32 = 2243;
|
|
const REAL_WORLD_VARIABLE_4_CONDITION_ID: i32 = 2244;
|
|
const REAL_COMPANY_VARIABLE_1_CONDITION_ID: i32 = 2245;
|
|
const REAL_COMPANY_VARIABLE_2_CONDITION_ID: i32 = 2246;
|
|
const REAL_COMPANY_VARIABLE_3_CONDITION_ID: i32 = 2247;
|
|
const REAL_COMPANY_VARIABLE_4_CONDITION_ID: i32 = 2248;
|
|
const REAL_PLAYER_VARIABLE_1_CONDITION_ID: i32 = 2249;
|
|
const REAL_PLAYER_VARIABLE_2_CONDITION_ID: i32 = 2250;
|
|
const REAL_PLAYER_VARIABLE_3_CONDITION_ID: i32 = 2251;
|
|
const REAL_PLAYER_VARIABLE_4_CONDITION_ID: i32 = 2252;
|
|
const REAL_TERRITORY_VARIABLE_1_CONDITION_ID: i32 = 2253;
|
|
const REAL_TERRITORY_VARIABLE_2_CONDITION_ID: i32 = 2254;
|
|
const REAL_TERRITORY_VARIABLE_3_CONDITION_ID: i32 = 2255;
|
|
const REAL_TERRITORY_VARIABLE_4_CONDITION_ID: i32 = 2256;
|
|
const REAL_CHAIRMAN_CASH_CONDITION_ID: i32 = 2218;
|
|
const REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID: i32 = 2239;
|
|
const REAL_CHAIRMAN_NET_WORTH_CONDITION_ID: i32 = 2240;
|
|
const REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID: i32 = 1247;
|
|
const REAL_INVESTOR_CONFIDENCE_CONDITION_ID: i32 = 2366;
|
|
const REAL_CREDIT_RATING_CONDITION_ID: i32 = 2367;
|
|
const REAL_PRIME_RATE_CONDITION_ID: i32 = 2368;
|
|
const REAL_MANAGEMENT_ATTITUDE_CONDITION_ID: i32 = 2369;
|
|
const REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID: i32 = 2620;
|
|
const REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID: i32 = 200;
|
|
const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422;
|
|
const REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID: i32 = 2423;
|
|
const REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2418;
|
|
const REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2419;
|
|
const REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2420;
|
|
const REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2421;
|
|
const REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547;
|
|
const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516;
|
|
|
|
const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 56] = [
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_WORLD_VARIABLE_1_CONDITION_ID,
|
|
label: "Game Variable 1",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(1)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_WORLD_VARIABLE_2_CONDITION_ID,
|
|
label: "Game Variable 2",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(2)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_WORLD_VARIABLE_3_CONDITION_ID,
|
|
label: "Game Variable 3",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(3)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_WORLD_VARIABLE_4_CONDITION_ID,
|
|
label: "Game Variable 4",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(4)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_COMPANY_VARIABLE_1_CONDITION_ID,
|
|
label: "Company Variable 1",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(1)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_COMPANY_VARIABLE_2_CONDITION_ID,
|
|
label: "Company Variable 2",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(2)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_COMPANY_VARIABLE_3_CONDITION_ID,
|
|
label: "Company Variable 3",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(3)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_COMPANY_VARIABLE_4_CONDITION_ID,
|
|
label: "Company Variable 4",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(4)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_PLAYER_VARIABLE_1_CONDITION_ID,
|
|
label: "Player Variable 1",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(1)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_PLAYER_VARIABLE_2_CONDITION_ID,
|
|
label: "Player Variable 2",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(2)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_PLAYER_VARIABLE_3_CONDITION_ID,
|
|
label: "Player Variable 3",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(3)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_PLAYER_VARIABLE_4_CONDITION_ID,
|
|
label: "Player Variable 4",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(4)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_TERRITORY_VARIABLE_1_CONDITION_ID,
|
|
label: "Territory Variable 1",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(1)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_TERRITORY_VARIABLE_2_CONDITION_ID,
|
|
label: "Territory Variable 2",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(2)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_TERRITORY_VARIABLE_3_CONDITION_ID,
|
|
label: "Territory Variable 3",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(3)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_TERRITORY_VARIABLE_4_CONDITION_ID,
|
|
label: "Territory Variable 4",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(4)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 1802,
|
|
label: "Current Cash",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::CurrentCash,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID,
|
|
label: "Player Cash",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(
|
|
RuntimeChairmanMetric::CurrentCash,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID,
|
|
label: "Player Stock Value",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(
|
|
RuntimeChairmanMetric::HoldingsValueTotal,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_CHAIRMAN_NET_WORTH_CONDITION_ID,
|
|
label: "Player Net Worth",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(
|
|
RuntimeChairmanMetric::NetWorthTotal,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID,
|
|
label: "Purchasing Power",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(
|
|
RuntimeChairmanMetric::PurchasingPowerTotal,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 951,
|
|
label: "Total Debt",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::TotalDebt,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_INVESTOR_CONFIDENCE_CONDITION_ID,
|
|
label: "Investor Confidence",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::InvestorConfidence,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_CREDIT_RATING_CONDITION_ID,
|
|
label: "Credit Rating",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::CreditRating,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_PRIME_RATE_CONDITION_ID,
|
|
label: "Prime Rate",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::PrimeRate,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_MANAGEMENT_ATTITUDE_CONDITION_ID,
|
|
label: "Management Attitude",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::ManagementAttitude,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID,
|
|
label: "Book Value Per Share",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::BookValuePerShare,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2293,
|
|
label: "Company Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::TrackPiecesTotal,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2294,
|
|
label: "Company Single Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::TrackPiecesSingle,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2295,
|
|
label: "Company Double Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::TrackPiecesDouble,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2296,
|
|
label: "Company Transition Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::TrackPiecesTransition,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2297,
|
|
label: "Company Electric Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::TrackPiecesElectric,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2298,
|
|
label: "Company Non-Electric Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
|
|
RuntimeCompanyMetric::TrackPiecesNonElectric,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2313,
|
|
label: "Territory Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(
|
|
RuntimeTerritoryMetric::TrackPiecesTotal,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2314,
|
|
label: "Territory Single Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(
|
|
RuntimeTerritoryMetric::TrackPiecesSingle,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2315,
|
|
label: "Territory Double Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(
|
|
RuntimeTerritoryMetric::TrackPiecesDouble,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2316,
|
|
label: "Territory Transition Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(
|
|
RuntimeTerritoryMetric::TrackPiecesTransition,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2317,
|
|
label: "Territory Electric Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(
|
|
RuntimeTerritoryMetric::TrackPiecesElectric,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2318,
|
|
label: "Territory Non-Electric Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(
|
|
RuntimeTerritoryMetric::TrackPiecesNonElectric,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2323,
|
|
label: "Company-Territory Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
|
|
RuntimeTrackMetric::Total,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2324,
|
|
label: "Company-Territory Single Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
|
|
RuntimeTrackMetric::Single,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2325,
|
|
label: "Company-Territory Double Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
|
|
RuntimeTrackMetric::Double,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2326,
|
|
label: "Company-Territory Transition Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
|
|
RuntimeTrackMetric::Transition,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2327,
|
|
label: "Company-Territory Electric Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
|
|
RuntimeTrackMetric::Electric,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2328,
|
|
label: "Company-Territory Non-Electric Track Pieces",
|
|
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
|
|
RuntimeTrackMetric::NonElectric,
|
|
)),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID,
|
|
label: "%1 Avail.",
|
|
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID,
|
|
label: "%1 Production",
|
|
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID,
|
|
label: "Unknown Loco Available",
|
|
kind: RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::NamedLocomotiveAvailability,
|
|
),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID,
|
|
label: "Unknown Loco Cost",
|
|
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID,
|
|
label: "All Cargo Production",
|
|
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID,
|
|
label: "All Factory Production",
|
|
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID,
|
|
label: "All Farm/Mine Production",
|
|
kind: RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::FarmMineProductionTotal,
|
|
),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID,
|
|
label: "Unknown Cargo Production",
|
|
kind: RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::OtherCargoProductionTotal,
|
|
),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID,
|
|
label: "Limited Track Building Amount",
|
|
kind: RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::LimitedTrackBuildingAmount,
|
|
),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: REAL_TERRITORY_ACCESS_COST_CONDITION_ID,
|
|
label: "Access Rights Cost:",
|
|
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost),
|
|
},
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id: 2350,
|
|
label: "Economic Status",
|
|
kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus),
|
|
},
|
|
];
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
struct KnownSpecialConditionDefinition {
|
|
slot_index: u8,
|
|
hidden: bool,
|
|
label_id: u32,
|
|
help_id: u32,
|
|
label: &'static str,
|
|
}
|
|
|
|
const KNOWN_SPECIAL_CONDITION_DEFINITIONS: [KnownSpecialConditionDefinition;
|
|
SPECIAL_CONDITION_COUNT] = [
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 0,
|
|
hidden: false,
|
|
label_id: 2535,
|
|
help_id: 2564,
|
|
label: "Disable Stock Buying and Selling",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 1,
|
|
hidden: false,
|
|
label_id: 2536,
|
|
help_id: 2565,
|
|
label: "Disable Margin Buying/Short Selling Stock",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 2,
|
|
hidden: false,
|
|
label_id: 2537,
|
|
help_id: 2566,
|
|
label: "Disable Company Issue/Buy Back Stock",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 3,
|
|
hidden: false,
|
|
label_id: 2538,
|
|
help_id: 2567,
|
|
label: "Disable Issuing/Repaying Bonds",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 4,
|
|
hidden: false,
|
|
label_id: 2539,
|
|
help_id: 2568,
|
|
label: "Disable Declaring Bankruptcy",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 5,
|
|
hidden: false,
|
|
label_id: 2540,
|
|
help_id: 2569,
|
|
label: "Disable Changing the Dividend Rate",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 6,
|
|
hidden: false,
|
|
label_id: 2541,
|
|
help_id: 2570,
|
|
label: "Disable Replacing a Locomotive",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 7,
|
|
hidden: false,
|
|
label_id: 2542,
|
|
help_id: 2571,
|
|
label: "Disable Retiring a Train",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 8,
|
|
hidden: false,
|
|
label_id: 2543,
|
|
help_id: 2572,
|
|
label: "Disable Changing Cargo Consist On Train",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 9,
|
|
hidden: false,
|
|
label_id: 2544,
|
|
help_id: 2573,
|
|
label: "Disable Buying a Train",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 10,
|
|
hidden: false,
|
|
label_id: 2545,
|
|
help_id: 2574,
|
|
label: "Disable All Track Building",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 11,
|
|
hidden: false,
|
|
label_id: 2546,
|
|
help_id: 2575,
|
|
label: "Disable Unconnected Track Building",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 12,
|
|
hidden: false,
|
|
label_id: 2547,
|
|
help_id: 2576,
|
|
label: "Limited Track Building Amount",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 13,
|
|
hidden: false,
|
|
label_id: 2548,
|
|
help_id: 2577,
|
|
label: "Disable Building Stations",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 14,
|
|
hidden: false,
|
|
label_id: 2549,
|
|
help_id: 2578,
|
|
label: "Disable Building Hotel/Restaurant/Tavern/Post Office",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 15,
|
|
hidden: false,
|
|
label_id: 2550,
|
|
help_id: 2579,
|
|
label: "Disable Building Customs House",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 16,
|
|
hidden: false,
|
|
label_id: 2551,
|
|
help_id: 2580,
|
|
label: "Disable Building Industry Buildings",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 17,
|
|
hidden: false,
|
|
label_id: 2552,
|
|
help_id: 2581,
|
|
label: "Disable Buying Existing Industry Buildings",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 18,
|
|
hidden: false,
|
|
label_id: 2553,
|
|
help_id: 2582,
|
|
label: "Disable Being Fired As Chairman",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 19,
|
|
hidden: false,
|
|
label_id: 2554,
|
|
help_id: 2583,
|
|
label: "Disable Resigning as Chairman",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 20,
|
|
hidden: false,
|
|
label_id: 2555,
|
|
help_id: 2584,
|
|
label: "Disable Chairmanship Takeover",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 21,
|
|
hidden: false,
|
|
label_id: 2556,
|
|
help_id: 2585,
|
|
label: "Disable Starting Any Companies",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 22,
|
|
hidden: false,
|
|
label_id: 2557,
|
|
help_id: 2586,
|
|
label: "Disable Starting Multiple Companies",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 23,
|
|
hidden: false,
|
|
label_id: 2558,
|
|
help_id: 2587,
|
|
label: "Disable Merging Companies",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 24,
|
|
hidden: false,
|
|
label_id: 2559,
|
|
help_id: 2588,
|
|
label: "Disable Bulldozing",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 25,
|
|
hidden: false,
|
|
label_id: 2560,
|
|
help_id: 2589,
|
|
label: "Show Visited Track",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 26,
|
|
hidden: false,
|
|
label_id: 2561,
|
|
help_id: 2590,
|
|
label: "Show Visited Stations",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 27,
|
|
hidden: false,
|
|
label_id: 2562,
|
|
help_id: 2591,
|
|
label: "Use Slow Date",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 28,
|
|
hidden: false,
|
|
label_id: 2563,
|
|
help_id: 2592,
|
|
label: "Completely Disable Money-Related Things",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 29,
|
|
hidden: false,
|
|
label_id: 2874,
|
|
help_id: 2875,
|
|
label: "Use Bio-Accelerator Cars",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 30,
|
|
hidden: false,
|
|
label_id: 3722,
|
|
help_id: 3723,
|
|
label: "Disable Cargo Economy",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 31,
|
|
hidden: false,
|
|
label_id: 3835,
|
|
help_id: 3836,
|
|
label: "Use Wartime Cargos",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 32,
|
|
hidden: false,
|
|
label_id: 3850,
|
|
help_id: 3851,
|
|
label: "Disable Train Crashes",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 33,
|
|
hidden: false,
|
|
label_id: 3852,
|
|
help_id: 3853,
|
|
label: "Disable Train Crashes AND Breakdowns",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 34,
|
|
hidden: false,
|
|
label_id: 3920,
|
|
help_id: 3921,
|
|
label: "AI Ignore Territories At Startup",
|
|
},
|
|
KnownSpecialConditionDefinition {
|
|
slot_index: 35,
|
|
hidden: true,
|
|
label_id: 3,
|
|
help_id: 3,
|
|
label: "Hidden sentinel",
|
|
},
|
|
];
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
struct KnownTagDefinition {
|
|
tag_id: u16,
|
|
label: &'static str,
|
|
grounded_meaning: &'static str,
|
|
}
|
|
|
|
const KNOWN_TAG_DEFINITIONS: [KnownTagDefinition; 4] = [
|
|
KnownTagDefinition {
|
|
tag_id: 0x2cee,
|
|
label: "overlay_mask_plane_primary",
|
|
grounded_meaning: "Primary one-byte overlay mask plane restored into world offset +0x1655.",
|
|
},
|
|
KnownTagDefinition {
|
|
tag_id: 0x2d51,
|
|
label: "overlay_mask_plane_secondary",
|
|
grounded_meaning: "Secondary one-byte overlay mask plane restored into world offset +0x1659.",
|
|
},
|
|
KnownTagDefinition {
|
|
tag_id: 0x9471,
|
|
label: "sidecar_byte_plane_family_low",
|
|
grounded_meaning: "Lower bound of the grounded sidecar byte-plane chunk family.",
|
|
},
|
|
KnownTagDefinition {
|
|
tag_id: 0x9472,
|
|
label: "sidecar_byte_plane_family_high",
|
|
grounded_meaning: "Upper bound of the grounded sidecar byte-plane chunk family.",
|
|
},
|
|
];
|
|
|
|
fn known_special_condition_definition_for_label_id(
|
|
label_id: u32,
|
|
) -> Option<KnownSpecialConditionDefinition> {
|
|
KNOWN_SPECIAL_CONDITION_DEFINITIONS
|
|
.iter()
|
|
.copied()
|
|
.find(|definition| !definition.hidden && definition.label_id == label_id)
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpKnownTagHit {
|
|
pub tag_id: u16,
|
|
pub tag_hex: String,
|
|
pub label: String,
|
|
pub grounded_meaning: String,
|
|
pub hit_count: usize,
|
|
pub sample_offsets: Vec<usize>,
|
|
pub last_offset: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPreambleWord {
|
|
pub index: usize,
|
|
pub offset: usize,
|
|
pub value_le: u32,
|
|
pub value_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPreamble {
|
|
pub byte_len: usize,
|
|
pub word_count: usize,
|
|
pub words: Vec<SmpPreambleWord>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpAsciiPreview {
|
|
pub offset: usize,
|
|
pub byte_len: usize,
|
|
pub preview: String,
|
|
pub truncated: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSharedHeader {
|
|
pub byte_len: usize,
|
|
pub root_kind_word: u32,
|
|
pub root_kind_word_hex: String,
|
|
pub primary_family_tag: u32,
|
|
pub primary_family_tag_hex: String,
|
|
pub shared_signature_words_1_to_7: Vec<u32>,
|
|
pub shared_signature_hex_words_1_to_7: Vec<String>,
|
|
pub matches_grounded_common_signature: bool,
|
|
pub payload_window_words_8_to_9: Vec<u32>,
|
|
pub payload_window_hex_words_8_to_9: Vec<String>,
|
|
pub reserved_words_10_to_14: Vec<u32>,
|
|
pub reserved_words_10_to_14_all_zero: bool,
|
|
pub final_flag_word: u32,
|
|
pub final_flag_word_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpHeaderVariantProbe {
|
|
pub variant_family: String,
|
|
pub variant_evidence: Vec<String>,
|
|
pub is_known_family: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpEarlyContentProbe {
|
|
pub first_post_text_nonzero_offset: usize,
|
|
pub zero_pad_after_text_len: usize,
|
|
pub first_post_text_block_len: usize,
|
|
pub first_post_text_block_hex: String,
|
|
pub trailing_zero_pad_after_first_block_len: usize,
|
|
pub secondary_nonzero_offset: Option<usize>,
|
|
pub secondary_aligned_word_window_offset: Option<usize>,
|
|
pub secondary_aligned_word_window_words: Vec<u32>,
|
|
pub secondary_aligned_word_window_hex_words: Vec<String>,
|
|
pub secondary_preview_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSecondaryVariantProbe {
|
|
pub aligned_window_offset: usize,
|
|
pub words: Vec<u32>,
|
|
pub hex_words: Vec<String>,
|
|
pub variant_family: String,
|
|
pub variant_evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpContainerProfile {
|
|
pub profile_family: String,
|
|
pub profile_evidence: Vec<String>,
|
|
pub is_known_profile: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveBootstrapBlock {
|
|
pub profile_family: String,
|
|
pub aligned_window_offset: usize,
|
|
pub leading_word: u32,
|
|
pub leading_word_hex: String,
|
|
pub anchor_word: u32,
|
|
pub anchor_word_hex: String,
|
|
pub descriptor_word_2: u32,
|
|
pub descriptor_word_2_hex: String,
|
|
pub descriptor_word_3: u32,
|
|
pub descriptor_word_3_hex: String,
|
|
pub descriptor_word_4: u32,
|
|
pub descriptor_word_4_hex: String,
|
|
pub descriptor_word_5: u32,
|
|
pub descriptor_word_5_hex: String,
|
|
pub descriptor_word_6: u32,
|
|
pub descriptor_word_6_hex: String,
|
|
pub descriptor_word_7: u32,
|
|
pub descriptor_word_7_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveAnchorRunBlock {
|
|
pub profile_family: String,
|
|
pub cycle_start_offset: usize,
|
|
pub cycle_words: Vec<u32>,
|
|
pub cycle_hex_words: Vec<String>,
|
|
pub full_cycle_count: usize,
|
|
pub partial_cycle_word_count: usize,
|
|
pub trailer_offset: usize,
|
|
pub trailer_words: Vec<u32>,
|
|
pub trailer_hex_words: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRuntimeAnchorCycleBlock {
|
|
pub profile_family: String,
|
|
pub cycle_start_offset: usize,
|
|
pub cycle_words: Vec<u32>,
|
|
pub cycle_hex_words: Vec<String>,
|
|
pub full_cycle_count: usize,
|
|
pub partial_cycle_word_count: usize,
|
|
pub trailer_offset: usize,
|
|
pub trailer_words: Vec<u32>,
|
|
pub trailer_hex_words: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRuntimeTrailerBlock {
|
|
pub profile_family: String,
|
|
pub trailer_family: String,
|
|
pub trailer_evidence: Vec<String>,
|
|
pub trailer_offset: usize,
|
|
pub prefix_words_0_to_5: Vec<u32>,
|
|
pub prefix_hex_words_0_to_5: Vec<String>,
|
|
pub tag_word_6: u32,
|
|
pub tag_word_6_hex: String,
|
|
pub tag_chunk_id_u16: u16,
|
|
pub tag_chunk_id_hex: String,
|
|
pub tag_chunk_id_grounded_alignment: Option<String>,
|
|
pub length_word_7: u32,
|
|
pub length_word_7_hex: String,
|
|
pub length_high_u16: u16,
|
|
pub length_high_hex: String,
|
|
pub selector_word_8: u32,
|
|
pub selector_word_8_hex: String,
|
|
pub selector_high_u16: u16,
|
|
pub selector_high_hex: String,
|
|
pub layout_word_9: u32,
|
|
pub layout_word_9_hex: String,
|
|
pub descriptor_word_10: u32,
|
|
pub descriptor_word_10_hex: String,
|
|
pub descriptor_high_u16: u16,
|
|
pub descriptor_high_hex: String,
|
|
pub descriptor_word_11: u32,
|
|
pub descriptor_word_11_hex: String,
|
|
pub counter_word_12: u32,
|
|
pub counter_word_12_hex: String,
|
|
pub offset_word_13: u32,
|
|
pub offset_word_13_hex: String,
|
|
pub span_word_14: u32,
|
|
pub span_word_14_hex: String,
|
|
pub mode_word_15: u32,
|
|
pub mode_word_15_hex: String,
|
|
pub words: Vec<u32>,
|
|
pub hex_words: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRuntimePostSpanProbe {
|
|
pub profile_family: String,
|
|
pub span_target_offset: usize,
|
|
pub next_nonzero_offset: Option<usize>,
|
|
pub next_aligned_candidate_offset: Option<usize>,
|
|
pub next_aligned_candidate_words: Vec<u32>,
|
|
pub next_aligned_candidate_hex_words: Vec<String>,
|
|
pub header_candidates: Vec<SmpRuntimePostSpanHeaderCandidate>,
|
|
pub grounded_progress_hits: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRuntimePostSpanHeaderCandidate {
|
|
pub offset: usize,
|
|
pub words: Vec<u32>,
|
|
pub hex_words: Vec<String>,
|
|
pub dense_word_count: usize,
|
|
pub high_u16_words: Vec<u16>,
|
|
pub high_hex_words: Vec<String>,
|
|
pub grounded_alignments: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRt3105PostSpanBridgeProbe {
|
|
pub profile_family: String,
|
|
pub bridge_family: String,
|
|
pub bridge_evidence: Vec<String>,
|
|
pub span_target_offset: usize,
|
|
pub next_candidate_offset: Option<usize>,
|
|
pub next_candidate_delta_from_span_target: Option<usize>,
|
|
pub packed_profile_offset: usize,
|
|
pub packed_profile_delta_from_span_target: usize,
|
|
pub next_candidate_delta_from_packed_profile: Option<i64>,
|
|
pub selector_high_u16: u16,
|
|
pub selector_high_hex: String,
|
|
pub descriptor_high_u16: u16,
|
|
pub descriptor_high_hex: String,
|
|
pub next_candidate_high_u16_words: Vec<u16>,
|
|
pub next_candidate_high_hex_words: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRt3105SaveBridgePayloadProbe {
|
|
pub profile_family: String,
|
|
pub bridge_family: String,
|
|
pub primary_block_offset: usize,
|
|
pub primary_block_len: usize,
|
|
pub primary_block_len_hex: String,
|
|
pub primary_words: Vec<u32>,
|
|
pub primary_hex_words: Vec<String>,
|
|
pub secondary_block_offset: usize,
|
|
pub secondary_block_delta_from_primary: usize,
|
|
pub secondary_block_delta_from_primary_hex: String,
|
|
pub secondary_block_end_offset: usize,
|
|
pub secondary_block_len: usize,
|
|
pub secondary_block_len_hex: String,
|
|
pub secondary_preview_word_count: usize,
|
|
pub secondary_words: Vec<u32>,
|
|
pub secondary_hex_words: Vec<String>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveWorldSelectionContextProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub chunk_tag_offset: usize,
|
|
pub payload_offset: usize,
|
|
pub payload_len: usize,
|
|
pub payload_len_hex: String,
|
|
pub selected_company_id_offset: usize,
|
|
pub selected_company_id: u32,
|
|
pub selected_company_id_hex: String,
|
|
pub selected_chairman_profile_id_offset: usize,
|
|
pub selected_chairman_profile_id: u32,
|
|
pub selected_chairman_profile_id_hex: String,
|
|
pub chairman_slot_selector_offset: usize,
|
|
pub chairman_slot_selectors: Vec<u8>,
|
|
pub campaign_override_flag_offset: usize,
|
|
pub campaign_override_flag: u8,
|
|
pub campaign_override_flag_hex: String,
|
|
pub chairman_role_gate_offset: usize,
|
|
pub chairman_role_gate_bytes: Vec<u8>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveWorldEconomicTuningProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub chunk_tag_offset: usize,
|
|
pub payload_offset: usize,
|
|
pub payload_len: usize,
|
|
pub payload_len_hex: String,
|
|
pub mirror_lane: SmpSaveDwordCandidate,
|
|
pub tuning_lanes: Vec<SmpSaveDwordCandidate>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveWorldIssue37Probe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub chunk_tag_offset: usize,
|
|
pub payload_offset: usize,
|
|
pub payload_len: usize,
|
|
pub payload_len_hex: String,
|
|
pub issue_37_raw_u8: u8,
|
|
pub issue_37_raw_hex: String,
|
|
pub issue_38_raw_u8: u8,
|
|
pub issue_38_raw_hex: String,
|
|
pub issue_39_raw_u8: u8,
|
|
pub issue_39_raw_hex: String,
|
|
pub issue_3a_raw_u8: u8,
|
|
pub issue_3a_raw_hex: String,
|
|
pub issue_value_lane: SmpSaveDwordCandidate,
|
|
pub multiplier_lane: SmpSaveDwordCandidate,
|
|
#[serde(default)]
|
|
pub issue_opinion_base_terms_raw_i32: Vec<i32>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveWorldFinanceNeighborhoodProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub chunk_tag_offset: usize,
|
|
pub payload_offset: usize,
|
|
pub payload_len: usize,
|
|
pub payload_len_hex: String,
|
|
pub packed_year_word_raw_u16: u16,
|
|
pub packed_year_word_raw_hex: String,
|
|
pub partial_year_progress_raw_u8: u8,
|
|
pub partial_year_progress_raw_hex: String,
|
|
pub current_calendar_tuple_word_lane: SmpSaveDwordCandidate,
|
|
pub current_calendar_tuple_word_2_lane: SmpSaveDwordCandidate,
|
|
pub absolute_counter_lane: SmpSaveDwordCandidate,
|
|
pub absolute_counter_mirror_lane: SmpSaveDwordCandidate,
|
|
pub stock_policy_raw_u8: u8,
|
|
pub stock_policy_raw_hex: String,
|
|
pub bond_policy_raw_u8: u8,
|
|
pub bond_policy_raw_hex: String,
|
|
pub bankruptcy_policy_raw_u8: u8,
|
|
pub bankruptcy_policy_raw_hex: String,
|
|
pub dividend_policy_raw_u8: u8,
|
|
pub dividend_policy_raw_hex: String,
|
|
pub building_density_growth_setting_lane: SmpSaveDwordCandidate,
|
|
pub dword_candidates: Vec<SmpSaveDwordCandidate>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveTaggedCollectionHeaderProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub metadata_tag_offset: usize,
|
|
pub records_tag_offset: usize,
|
|
pub close_tag_offset: usize,
|
|
pub direct_collection_flag: u32,
|
|
pub direct_collection_flag_hex: String,
|
|
pub direct_record_stride: u32,
|
|
pub direct_record_stride_hex: String,
|
|
pub live_id_bound: u32,
|
|
pub live_id_bound_hex: String,
|
|
pub live_record_count: u32,
|
|
pub live_record_count_hex: String,
|
|
pub header_words: Vec<u32>,
|
|
pub header_hex_words: Vec<String>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveUnclassifiedTaggedCollectionHeaderProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub metadata_tag: u32,
|
|
pub metadata_tag_hex: String,
|
|
pub records_tag: u32,
|
|
pub records_tag_hex: String,
|
|
pub close_tag: u32,
|
|
pub close_tag_hex: String,
|
|
pub metadata_tag_offset: usize,
|
|
pub records_tag_offset: usize,
|
|
pub close_tag_offset: usize,
|
|
pub records_span_len: usize,
|
|
pub direct_collection_flag: u32,
|
|
pub direct_collection_flag_hex: String,
|
|
pub direct_record_stride: u32,
|
|
pub direct_record_stride_hex: String,
|
|
pub live_id_bound: u32,
|
|
pub live_id_bound_hex: String,
|
|
pub live_record_count: u32,
|
|
pub live_record_count_hex: String,
|
|
pub header_words: Vec<u32>,
|
|
pub header_hex_words: Vec<String>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveTrainCollectionDirectoryEntryProbe {
|
|
pub live_entry_id: u32,
|
|
pub payload_relative_offset: u32,
|
|
pub payload_relative_offset_hex: String,
|
|
pub payload_absolute_offset: usize,
|
|
pub previous_live_entry_id: u32,
|
|
pub previous_live_entry_id_hex: String,
|
|
pub next_live_entry_id: u32,
|
|
pub next_live_entry_id_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveTrainCollectionDirectoryProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub metadata_tag_offset: usize,
|
|
pub records_tag_offset: usize,
|
|
pub close_tag_offset: usize,
|
|
pub directory_root_dword_index: usize,
|
|
pub directory_entry_dword_count: usize,
|
|
pub live_record_count: u32,
|
|
pub live_id_bound: u32,
|
|
#[serde(default)]
|
|
pub chain_head_live_entry_id: Option<u32>,
|
|
#[serde(default)]
|
|
pub chain_tail_live_entry_id: Option<u32>,
|
|
pub entries: Vec<SmpSaveTrainCollectionDirectoryEntryProbe>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveRegionProfileEntryProbe {
|
|
pub entry_index: usize,
|
|
pub row_relative_offset: usize,
|
|
pub name: String,
|
|
pub trailing_weight_f32: f32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveRegionProfileCollectionProbe {
|
|
pub direct_collection_flag: u32,
|
|
pub entry_stride: u32,
|
|
pub live_id_bound: u32,
|
|
pub live_record_count: u32,
|
|
pub entry_start_relative_offset: usize,
|
|
pub trailing_padding_len: usize,
|
|
#[serde(default)]
|
|
pub entries: Vec<SmpSaveRegionProfileEntryProbe>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveRegionRecordTripletEntryProbe {
|
|
pub record_index: usize,
|
|
pub name: String,
|
|
pub name_tag_relative_offset: usize,
|
|
pub policy_tag_relative_offset: usize,
|
|
pub profile_tag_relative_offset: usize,
|
|
pub policy_chunk_len: usize,
|
|
pub profile_chunk_len: usize,
|
|
pub policy_leading_f32_0: f32,
|
|
pub policy_leading_f32_1: f32,
|
|
pub policy_leading_f32_2: f32,
|
|
#[serde(default)]
|
|
pub policy_reserved_dwords: Vec<u32>,
|
|
pub policy_trailing_word: u16,
|
|
pub policy_trailing_word_hex: String,
|
|
#[serde(default)]
|
|
pub profile_collection: Option<SmpSaveRegionProfileCollectionProbe>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveRegionRecordTripletProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub records_tag_offset: usize,
|
|
pub close_tag_offset: usize,
|
|
pub record_count: usize,
|
|
#[serde(default)]
|
|
pub entries: Vec<SmpSaveRegionRecordTripletEntryProbe>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveRegionQueuedNoticeRecordEntryProbe {
|
|
pub node_base_offset: usize,
|
|
pub payload_seed_offset: usize,
|
|
pub next_link_raw: u32,
|
|
pub next_link_raw_hex: String,
|
|
pub payload_seed_dword: u32,
|
|
pub payload_seed_dword_hex: String,
|
|
pub kind: u32,
|
|
pub kind_hex: String,
|
|
pub promotion_latch_dword: u32,
|
|
pub promotion_latch_dword_hex: String,
|
|
pub region_id: u32,
|
|
pub region_id_hex: String,
|
|
pub amount: u32,
|
|
pub amount_hex: String,
|
|
pub trailing_sentinel_i32_0: i32,
|
|
pub trailing_sentinel_i32_0_hex: String,
|
|
pub trailing_sentinel_i32_1: i32,
|
|
pub trailing_sentinel_i32_1_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveRegionQueuedNoticeRecordProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub payload_seed_dword: u32,
|
|
pub payload_seed_dword_hex: String,
|
|
#[serde(default)]
|
|
pub entries: Vec<SmpSaveRegionQueuedNoticeRecordEntryProbe>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSavePlacedStructureRecordTripletEntryProbe {
|
|
pub record_index: usize,
|
|
pub primary_name: String,
|
|
pub secondary_name: String,
|
|
pub name_tag_relative_offset: usize,
|
|
pub policy_tag_relative_offset: usize,
|
|
pub profile_tag_relative_offset: usize,
|
|
pub policy_chunk_len: usize,
|
|
pub profile_chunk_len: usize,
|
|
pub policy_f32_lane_0: f32,
|
|
pub policy_f32_lane_1: f32,
|
|
pub policy_f32_lane_2: f32,
|
|
pub policy_f32_lane_3: f32,
|
|
pub policy_f32_lane_4: f32,
|
|
pub policy_reserved_dword: u32,
|
|
pub policy_trailing_word: u16,
|
|
pub policy_trailing_word_hex: String,
|
|
pub profile_open_marker: u32,
|
|
pub profile_open_marker_hex: String,
|
|
pub profile_repeated_primary_name: String,
|
|
pub profile_repeated_secondary_name: String,
|
|
pub profile_payload_dword: u32,
|
|
pub profile_payload_dword_hex: String,
|
|
pub profile_sentinel_i32: i32,
|
|
pub profile_status_kind: String,
|
|
pub farm_growth_stage_index: Option<u8>,
|
|
pub profile_close_marker: u32,
|
|
pub profile_close_marker_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSavePlacedStructureRecordTripletProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub records_tag_offset: usize,
|
|
pub close_tag_offset: usize,
|
|
pub record_count: usize,
|
|
#[serde(default)]
|
|
pub entries: Vec<SmpSavePlacedStructureRecordTripletEntryProbe>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSavePlacedStructureDynamicSideBufferProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub metadata_tag_offset: usize,
|
|
pub records_tag_offset: usize,
|
|
pub close_tag_offset: usize,
|
|
pub records_span_len: usize,
|
|
pub direct_record_stride: u32,
|
|
pub direct_record_stride_hex: String,
|
|
pub live_id_bound: u32,
|
|
pub live_id_bound_hex: String,
|
|
pub live_record_count: u32,
|
|
pub live_record_count_hex: String,
|
|
pub owner_shared_dword: u32,
|
|
pub owner_shared_dword_hex: String,
|
|
pub owner_shared_dword_relative_offset: usize,
|
|
pub owner_shared_dword_matches_first_compact_prefix_leading_dword: bool,
|
|
pub prefix_leading_dword: u32,
|
|
pub prefix_leading_dword_hex: String,
|
|
pub prefix_trailing_word: u16,
|
|
pub prefix_trailing_word_hex: String,
|
|
pub prefix_separator_byte: u8,
|
|
pub prefix_separator_byte_hex: String,
|
|
pub first_embedded_name_tag_relative_offset: usize,
|
|
pub embedded_name_tag_count: usize,
|
|
pub decoded_embedded_name_row_count: usize,
|
|
pub decoded_embedded_name_row_with_tertiary_name_count: usize,
|
|
pub unique_compact_prefix_pattern_count: usize,
|
|
pub prefix_leading_dword_matching_embedded_profile_tag_count: usize,
|
|
pub unique_embedded_name_pair_count: usize,
|
|
#[serde(default)]
|
|
pub first_embedded_primary_name: Option<String>,
|
|
#[serde(default)]
|
|
pub first_embedded_secondary_name: Option<String>,
|
|
#[serde(default)]
|
|
pub first_embedded_tertiary_name: Option<String>,
|
|
#[serde(default)]
|
|
pub embedded_name_row_samples: Vec<SmpSavePlacedStructureDynamicSideBufferSampleEntry>,
|
|
#[serde(default)]
|
|
pub compact_prefix_pattern_summaries:
|
|
Vec<SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary>,
|
|
#[serde(default)]
|
|
pub name_pair_summaries: Vec<SmpSavePlacedStructureDynamicSideBufferNamePairSummary>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSavePlacedStructureDynamicSideBufferSampleEntry {
|
|
pub sample_index: usize,
|
|
pub name_tag_relative_offset: usize,
|
|
pub prefix_leading_dword: u32,
|
|
pub prefix_leading_dword_hex: String,
|
|
pub prefix_trailing_word: u16,
|
|
pub prefix_trailing_word_hex: String,
|
|
pub prefix_separator_byte: u8,
|
|
pub prefix_separator_byte_hex: String,
|
|
#[serde(default)]
|
|
pub primary_name: Option<String>,
|
|
#[serde(default)]
|
|
pub secondary_name: Option<String>,
|
|
#[serde(default)]
|
|
pub tertiary_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary {
|
|
pub prefix_leading_dword: u32,
|
|
pub prefix_leading_dword_hex: String,
|
|
pub prefix_trailing_word: u16,
|
|
pub prefix_trailing_word_hex: String,
|
|
pub prefix_separator_byte: u8,
|
|
pub prefix_separator_byte_hex: String,
|
|
pub count: usize,
|
|
pub first_name_tag_relative_offset: usize,
|
|
pub prefix_leading_dword_matches_embedded_profile_tag: bool,
|
|
pub section_like_primary_name_count: usize,
|
|
pub cap_like_primary_name_count: usize,
|
|
pub other_primary_name_count: usize,
|
|
#[serde(default)]
|
|
pub first_primary_name: Option<String>,
|
|
#[serde(default)]
|
|
pub first_secondary_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSavePlacedStructureDynamicSideBufferNamePairSummary {
|
|
pub primary_name: String,
|
|
pub secondary_name: String,
|
|
pub count: usize,
|
|
pub first_name_tag_relative_offset: usize,
|
|
pub unique_compact_prefix_pattern_count: usize,
|
|
pub dominant_prefix_leading_dword: u32,
|
|
pub dominant_prefix_leading_dword_hex: String,
|
|
pub dominant_prefix_trailing_word: u16,
|
|
pub dominant_prefix_trailing_word_hex: String,
|
|
pub dominant_prefix_separator_byte: u8,
|
|
pub dominant_prefix_separator_byte_hex: String,
|
|
pub dominant_prefix_count: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSavePlacedStructureDynamicSideBufferAlignmentProbe {
|
|
pub unique_side_buffer_name_pair_count: usize,
|
|
pub unique_triplet_name_pair_count: usize,
|
|
pub overlapping_name_pair_count: usize,
|
|
pub side_buffer_row_count: usize,
|
|
pub side_buffer_rows_with_matching_triplet_name_pair_count: usize,
|
|
pub side_buffer_rows_without_matching_triplet_name_pair_count: usize,
|
|
pub triplet_name_pairs_without_side_buffer_match_count: usize,
|
|
#[serde(default)]
|
|
pub matched_name_pair_samples: Vec<SmpSavePlacedStructureDynamicSideBufferNamePairSummary>,
|
|
#[serde(default)]
|
|
pub unmatched_side_buffer_name_pair_samples:
|
|
Vec<SmpSavePlacedStructureDynamicSideBufferNamePairSummary>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRt3105SaveNameTableProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub semantic_alignment: Vec<String>,
|
|
pub header_offset: usize,
|
|
pub header_word_0: u32,
|
|
pub header_word_0_hex: String,
|
|
pub header_word_1: u32,
|
|
pub header_word_1_hex: String,
|
|
pub header_word_2: u32,
|
|
pub header_word_2_hex: String,
|
|
pub entry_stride: usize,
|
|
pub entry_stride_hex: String,
|
|
pub header_prefix_word_count: usize,
|
|
pub observed_entry_capacity: usize,
|
|
pub observed_entry_count: usize,
|
|
pub zero_trailer_entry_count: usize,
|
|
pub nonzero_trailer_entry_count: usize,
|
|
pub distinct_trailer_words: Vec<u32>,
|
|
pub distinct_trailer_hex_words: Vec<String>,
|
|
pub zero_trailer_entry_names: Vec<String>,
|
|
pub entries_offset: usize,
|
|
pub entries_end_offset: usize,
|
|
pub trailing_footer_hex: String,
|
|
pub footer_progress_word_0: u32,
|
|
pub footer_progress_word_0_hex: String,
|
|
pub footer_progress_word_1: u32,
|
|
pub footer_progress_word_1_hex: String,
|
|
pub footer_trailing_byte: u8,
|
|
pub footer_trailing_byte_hex: String,
|
|
pub footer_grounded_alignments: Vec<String>,
|
|
pub entries: Vec<SmpRt3105SaveNameTableEntry>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRt3105SaveNamedLocomotiveAvailabilityProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub semantic_alignment: Vec<String>,
|
|
pub entries_offset: usize,
|
|
pub entry_stride: usize,
|
|
pub entry_stride_hex: String,
|
|
pub observed_entry_count: usize,
|
|
pub zero_availability_count: usize,
|
|
pub zero_availability_names: Vec<String>,
|
|
pub entries_end_offset: usize,
|
|
pub entries: Vec<SmpRt3105SaveNameTableEntry>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRt3105SaveNameTableEntry {
|
|
pub index: usize,
|
|
pub offset: usize,
|
|
pub text: String,
|
|
pub availability_dword: u32,
|
|
pub availability_dword_hex: String,
|
|
pub trailer_word: u32,
|
|
pub trailer_word_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSpecialConditionEntry {
|
|
pub slot_index: u8,
|
|
pub hidden: bool,
|
|
pub label_id: u32,
|
|
pub help_id: u32,
|
|
pub label: String,
|
|
pub value: u32,
|
|
pub value_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSpecialConditionsProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub table_offset: usize,
|
|
pub table_len: usize,
|
|
pub enabled_visible_count: usize,
|
|
pub enabled_visible_labels: Vec<String>,
|
|
pub hidden_sentinel_slot_index: u8,
|
|
pub hidden_sentinel_value: u32,
|
|
pub hidden_sentinel_value_hex: String,
|
|
pub entries: Vec<SmpSpecialConditionEntry>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpAlignedRuntimeRuleBandLane {
|
|
pub band_index: usize,
|
|
pub absolute_offset: usize,
|
|
pub relative_offset: usize,
|
|
pub absolute_offset_hex: String,
|
|
pub relative_offset_hex: String,
|
|
pub lane_kind: String,
|
|
pub known_label: Option<String>,
|
|
pub value: u32,
|
|
pub value_hex: String,
|
|
pub probable_f32_le: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpAlignedRuntimeRuleBandProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub band_offset: usize,
|
|
pub band_end_offset: usize,
|
|
pub band_len: usize,
|
|
pub band_len_hex: String,
|
|
pub dword_count: usize,
|
|
pub known_editor_rule_dword_count: usize,
|
|
pub trailing_scalar_index: usize,
|
|
pub trailing_scalar_offset: usize,
|
|
pub trailing_scalar_offset_hex: String,
|
|
pub post_window_overlap_start_index: usize,
|
|
pub post_window_overlap_dword_count: usize,
|
|
pub post_window_overlap_end_index: usize,
|
|
pub post_window_overlap_post_relative_offset_start_hex: String,
|
|
pub post_window_overlap_post_relative_offset_end_hex: String,
|
|
pub nonzero_post_window_overlap_band_indices: Vec<usize>,
|
|
pub nonzero_post_window_overlap_post_relative_offset_hexes: Vec<String>,
|
|
pub nonzero_lane_count: usize,
|
|
pub nonzero_band_indices: Vec<usize>,
|
|
pub nonzero_relative_offset_hexes: Vec<String>,
|
|
pub nonzero_lanes: Vec<SmpAlignedRuntimeRuleBandLane>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPostSpecialConditionsScalarLane {
|
|
pub absolute_offset: usize,
|
|
pub relative_offset: usize,
|
|
pub absolute_offset_hex: String,
|
|
pub relative_offset_hex: String,
|
|
pub value: u32,
|
|
pub value_hex: String,
|
|
pub probable_f32_le: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPostTextGroundedFieldObservation {
|
|
pub field_name: String,
|
|
pub runtime_object_offset: usize,
|
|
pub runtime_object_offset_hex: String,
|
|
pub file_offset: usize,
|
|
pub file_offset_hex: String,
|
|
pub field_width_bytes: usize,
|
|
pub field_width_bytes_hex: String,
|
|
pub raw_hex: String,
|
|
pub value_u8: Option<u8>,
|
|
pub value_u8_hex: Option<String>,
|
|
pub value_u32: Option<u32>,
|
|
pub value_u32_hex: Option<String>,
|
|
pub probable_f32_le: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPostTextFloatAlignmentCandidate {
|
|
pub grounded_field_name: String,
|
|
pub grounded_field_runtime_object_offset: usize,
|
|
pub grounded_field_runtime_object_offset_hex: String,
|
|
pub grounded_field_file_offset: usize,
|
|
pub grounded_field_file_offset_hex: String,
|
|
pub candidate_offset: usize,
|
|
pub candidate_offset_hex: String,
|
|
pub candidate_value: u32,
|
|
pub candidate_value_hex: String,
|
|
pub probable_f32_le: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPostTextFieldNeighborhoodProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub window_offset: usize,
|
|
pub window_end_offset: usize,
|
|
pub window_len: usize,
|
|
pub window_len_hex: String,
|
|
pub grounded_field_observations: Vec<SmpPostTextGroundedFieldObservation>,
|
|
pub one_byte_early_float_candidates: Vec<SmpPostTextFloatAlignmentCandidate>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLocomotivePolicyFieldObservation {
|
|
pub field_name: String,
|
|
pub runtime_object_offset: usize,
|
|
pub runtime_object_offset_hex: String,
|
|
pub file_offset: usize,
|
|
pub file_offset_hex: String,
|
|
pub field_width_bytes: usize,
|
|
pub field_width_bytes_hex: String,
|
|
pub raw_hex: String,
|
|
pub value_u8: Option<u8>,
|
|
pub value_u8_hex: Option<String>,
|
|
pub value_u32: Option<u32>,
|
|
pub value_u32_hex: Option<String>,
|
|
pub probable_f32_le: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLocomotivePolicyFloatAlignmentCandidate {
|
|
pub grounded_field_name: String,
|
|
pub grounded_field_runtime_object_offset: usize,
|
|
pub grounded_field_runtime_object_offset_hex: String,
|
|
pub grounded_field_file_offset: usize,
|
|
pub grounded_field_file_offset_hex: String,
|
|
pub candidate_offset: usize,
|
|
pub candidate_offset_hex: String,
|
|
pub candidate_value: u32,
|
|
pub candidate_value_hex: String,
|
|
pub probable_f32_le: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLocomotivePolicyNeighborhoodProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub window_offset: usize,
|
|
pub window_end_offset: usize,
|
|
pub window_len: usize,
|
|
pub window_len_hex: String,
|
|
pub grounded_field_observations: Vec<SmpLocomotivePolicyFieldObservation>,
|
|
pub three_byte_early_float_candidates: Vec<SmpLocomotivePolicyFloatAlignmentCandidate>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPreRecipeScalarPlateauLane {
|
|
pub absolute_offset: usize,
|
|
pub relative_offset: usize,
|
|
pub absolute_offset_hex: String,
|
|
pub relative_offset_hex: String,
|
|
pub value: u32,
|
|
pub value_hex: String,
|
|
pub probable_f32_le: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPreRecipeScalarPlateauProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub window_offset: usize,
|
|
pub window_end_offset: usize,
|
|
pub window_len: usize,
|
|
pub window_len_hex: String,
|
|
pub aligned_dword_count: usize,
|
|
pub family_signature: String,
|
|
pub nonzero_lanes: Vec<SmpPreRecipeScalarPlateauLane>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRecipeBookSummaryBook {
|
|
pub book_index: usize,
|
|
pub book_offset: usize,
|
|
pub book_offset_hex: String,
|
|
pub head_kind: String,
|
|
pub head_nonzero_byte_count: usize,
|
|
pub head_cdcd_byte_count: usize,
|
|
pub head_first_16_hex: String,
|
|
pub max_annual_production_offset: usize,
|
|
pub max_annual_production_offset_hex: String,
|
|
pub max_annual_production_word: u32,
|
|
pub max_annual_production_word_hex: String,
|
|
pub max_annual_production_probable_f32_le: Option<String>,
|
|
pub line_area_offset: usize,
|
|
pub line_area_offset_hex: String,
|
|
pub line_area_len: usize,
|
|
pub line_area_len_hex: String,
|
|
pub line_area_kind: String,
|
|
pub line_area_nonzero_byte_count: usize,
|
|
pub line_area_cdcd_byte_count: usize,
|
|
pub line_area_first_16_hex: String,
|
|
pub lines: Vec<SmpRecipeBookLineSummary>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRecipeBookLineSummary {
|
|
pub line_index: usize,
|
|
pub line_offset: usize,
|
|
pub line_offset_hex: String,
|
|
pub line_kind: String,
|
|
pub line_signature_kind: String,
|
|
pub imports_to_runtime_descriptor: bool,
|
|
pub runtime_import_branch_kind: String,
|
|
pub line_nonzero_byte_count: usize,
|
|
pub line_cdcd_byte_count: usize,
|
|
pub line_first_16_hex: String,
|
|
pub mode_word_offset: usize,
|
|
pub mode_word_offset_hex: String,
|
|
pub mode_word: u32,
|
|
pub mode_word_hex: String,
|
|
pub annual_amount_offset: usize,
|
|
pub annual_amount_offset_hex: String,
|
|
pub annual_amount_word: u32,
|
|
pub annual_amount_word_hex: String,
|
|
pub annual_amount_probable_f32_le: Option<String>,
|
|
pub supplied_cargo_token_offset: usize,
|
|
pub supplied_cargo_token_offset_hex: String,
|
|
pub supplied_cargo_token_word: u32,
|
|
pub supplied_cargo_token_word_hex: String,
|
|
pub supplied_cargo_token_layout_kind: String,
|
|
pub supplied_cargo_token_window_hex: String,
|
|
pub supplied_cargo_token_window_ascii: String,
|
|
pub supplied_cargo_token_active_in_runtime_import: bool,
|
|
pub supplied_cargo_token_probable_high16_ascii_stem: Option<String>,
|
|
pub demanded_cargo_token_offset: usize,
|
|
pub demanded_cargo_token_offset_hex: String,
|
|
pub demanded_cargo_token_word: u32,
|
|
pub demanded_cargo_token_word_hex: String,
|
|
pub demanded_cargo_token_layout_kind: String,
|
|
pub demanded_cargo_token_window_hex: String,
|
|
pub demanded_cargo_token_window_ascii: String,
|
|
pub demanded_cargo_token_active_in_runtime_import: bool,
|
|
pub demanded_cargo_token_probable_high16_ascii_stem: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRecipeBookSummaryProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub root_offset: usize,
|
|
pub root_offset_hex: String,
|
|
pub runtime_object_root_offset: usize,
|
|
pub runtime_object_root_offset_hex: String,
|
|
pub book_count: usize,
|
|
pub book_stride: usize,
|
|
pub book_stride_hex: String,
|
|
pub max_annual_production_relative_offset: usize,
|
|
pub max_annual_production_relative_offset_hex: String,
|
|
pub line_area_relative_offset: usize,
|
|
pub line_area_relative_offset_hex: String,
|
|
pub line_count: usize,
|
|
pub line_stride: usize,
|
|
pub line_stride_hex: String,
|
|
pub books: Vec<SmpRecipeBookSummaryBook>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPostSpecialConditionsScalarProbe {
|
|
pub profile_family: String,
|
|
pub source_kind: String,
|
|
pub window_offset: usize,
|
|
pub window_end_offset: usize,
|
|
pub window_len: usize,
|
|
pub window_len_hex: String,
|
|
pub dword_count: usize,
|
|
pub overlap_end_offset: usize,
|
|
pub overlap_end_offset_hex: String,
|
|
pub overlap_dword_count: usize,
|
|
pub overlap_nonzero_dword_count: usize,
|
|
pub overlap_nonzero_relative_offset_hexes: Vec<String>,
|
|
pub tail_offset: usize,
|
|
pub tail_offset_hex: String,
|
|
pub tail_len: usize,
|
|
pub tail_len_hex: String,
|
|
pub tail_dword_count: usize,
|
|
pub tail_runtime_object_offset: usize,
|
|
pub tail_runtime_object_offset_hex: String,
|
|
pub tail_runtime_object_end_offset: usize,
|
|
pub tail_runtime_object_end_offset_hex: String,
|
|
pub tail_runtime_object_validated_byte_mirror: bool,
|
|
pub tail_grounded_live_field_offset: usize,
|
|
pub tail_grounded_live_field_offset_hex: String,
|
|
pub tail_grounded_live_field_name: String,
|
|
pub tail_grounded_live_field_copy_len: usize,
|
|
pub tail_grounded_live_field_copy_len_hex: String,
|
|
pub tail_grounded_live_field_copy_end_offset: usize,
|
|
pub tail_grounded_live_field_copy_end_offset_hex: String,
|
|
pub tail_window_cuts_through_grounded_live_field: bool,
|
|
pub tail_grounded_live_field_remaining_file_window_offset: usize,
|
|
pub tail_grounded_live_field_remaining_file_window_offset_hex: String,
|
|
pub tail_grounded_live_field_remaining_file_window_len: usize,
|
|
pub tail_grounded_live_field_remaining_file_window_len_hex: String,
|
|
pub tail_grounded_live_field_remaining_file_window_nonzero_byte_count: usize,
|
|
pub tail_grounded_live_field_remaining_file_window_first_nonzero_offset: Option<usize>,
|
|
pub tail_grounded_live_field_remaining_file_window_first_nonzero_offset_hex: Option<String>,
|
|
pub tail_grounded_live_field_remaining_file_window_last_nonzero_offset: Option<usize>,
|
|
pub tail_grounded_live_field_remaining_file_window_last_nonzero_offset_hex: Option<String>,
|
|
pub tail_next_grounded_dword_field_offset: usize,
|
|
pub tail_next_grounded_dword_field_offset_hex: String,
|
|
pub tail_next_grounded_dword_field_file_offset: usize,
|
|
pub tail_next_grounded_dword_field_file_offset_hex: String,
|
|
pub tail_second_grounded_dword_field_offset: usize,
|
|
pub tail_second_grounded_dword_field_offset_hex: String,
|
|
pub tail_second_grounded_dword_field_file_offset: usize,
|
|
pub tail_second_grounded_dword_field_file_offset_hex: String,
|
|
pub post_text_field_file_alignment_matches_grounded_dword_fields: bool,
|
|
pub tail_nonzero_dword_count: usize,
|
|
pub tail_first_nonzero_offset: Option<usize>,
|
|
pub tail_first_nonzero_offset_hex: Option<String>,
|
|
pub tail_last_nonzero_offset: Option<usize>,
|
|
pub tail_last_nonzero_offset_hex: Option<String>,
|
|
pub tail_nonzero_relative_offset_hexes: Vec<String>,
|
|
pub nonzero_dword_count: usize,
|
|
pub first_nonzero_offset: Option<usize>,
|
|
pub first_nonzero_offset_hex: Option<String>,
|
|
pub last_nonzero_offset: Option<usize>,
|
|
pub last_nonzero_offset_hex: Option<String>,
|
|
pub nonzero_lanes: Vec<SmpPostSpecialConditionsScalarLane>,
|
|
pub evidence: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpClassicRehydrateProfileProbe {
|
|
pub profile_family: String,
|
|
pub progress_32dc_offset: usize,
|
|
pub progress_3714_offset: usize,
|
|
pub progress_3715_offset: usize,
|
|
pub packed_profile_offset: usize,
|
|
pub packed_profile_len: usize,
|
|
pub packed_profile_len_hex: String,
|
|
pub packed_profile_block: SmpClassicPackedProfileBlock,
|
|
pub ascii_runs: Vec<SmpAsciiPreview>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpClassicPackedProfileBlock {
|
|
pub relative_len: usize,
|
|
pub relative_len_hex: String,
|
|
pub leading_word_0: u32,
|
|
pub leading_word_0_hex: String,
|
|
pub trailing_zero_word_count_after_leading_word: usize,
|
|
pub map_path_offset: usize,
|
|
pub map_path: Option<String>,
|
|
pub display_name_offset: usize,
|
|
pub display_name: Option<String>,
|
|
pub profile_byte_0x77: u8,
|
|
pub profile_byte_0x77_hex: String,
|
|
pub profile_byte_0x82: u8,
|
|
pub profile_byte_0x82_hex: String,
|
|
pub profile_byte_0x97: u8,
|
|
pub profile_byte_0x97_hex: String,
|
|
pub profile_byte_0xc5: u8,
|
|
pub profile_byte_0xc5_hex: String,
|
|
pub stable_nonzero_words: Vec<SmpPackedProfileWordLane>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRt3105PackedProfileProbe {
|
|
pub profile_family: String,
|
|
pub packed_profile_offset: usize,
|
|
pub packed_profile_len: usize,
|
|
pub packed_profile_len_hex: String,
|
|
pub packed_profile_block: SmpRt3105PackedProfileBlock,
|
|
pub ascii_runs: Vec<SmpAsciiPreview>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRt3105PackedProfileBlock {
|
|
pub relative_len: usize,
|
|
pub relative_len_hex: String,
|
|
pub leading_word_0: u32,
|
|
pub leading_word_0_hex: String,
|
|
pub trailing_zero_word_count_after_leading_word: usize,
|
|
pub header_flag_word_3: u32,
|
|
pub header_flag_word_3_hex: String,
|
|
pub map_path_offset: usize,
|
|
pub map_path: Option<String>,
|
|
pub display_name_offset: usize,
|
|
pub display_name: Option<String>,
|
|
pub profile_byte_0x77: u8,
|
|
pub profile_byte_0x77_hex: String,
|
|
pub profile_byte_0x82: u8,
|
|
pub profile_byte_0x82_hex: String,
|
|
pub profile_byte_0x97: u8,
|
|
pub profile_byte_0x97_hex: String,
|
|
pub profile_byte_0xc5: u8,
|
|
pub profile_byte_0xc5_hex: String,
|
|
pub stable_nonzero_words: Vec<SmpPackedProfileWordLane>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveLoadCandidateTableSummary {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub observed_entry_count: usize,
|
|
pub zero_availability_count: usize,
|
|
pub zero_availability_names: Vec<String>,
|
|
pub footer_progress_hex_words: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveLoadSummary {
|
|
pub file_extension_hint: Option<String>,
|
|
pub container_profile_family: Option<String>,
|
|
pub mechanism_family: String,
|
|
pub mechanism_confidence: String,
|
|
pub packed_profile_kind: Option<String>,
|
|
pub packed_profile_family: Option<String>,
|
|
pub packed_profile_offset: Option<usize>,
|
|
pub packed_profile_len: Option<usize>,
|
|
pub map_path: Option<String>,
|
|
pub display_name: Option<String>,
|
|
pub profile_byte_0x77: Option<u8>,
|
|
pub profile_byte_0x77_hex: Option<String>,
|
|
pub profile_byte_0x82: Option<u8>,
|
|
pub profile_byte_0x82_hex: Option<String>,
|
|
pub profile_byte_0x97: Option<u8>,
|
|
pub profile_byte_0x97_hex: Option<String>,
|
|
pub profile_byte_0xc5: Option<u8>,
|
|
pub profile_byte_0xc5_hex: Option<String>,
|
|
pub trailer_family: Option<String>,
|
|
pub bridge_family: Option<String>,
|
|
pub candidate_table: Option<SmpSaveLoadCandidateTableSummary>,
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedProfile {
|
|
pub profile_kind: String,
|
|
pub profile_family: String,
|
|
pub packed_profile_offset: usize,
|
|
pub packed_profile_len: usize,
|
|
pub packed_profile_len_hex: String,
|
|
pub leading_word_0: u32,
|
|
pub leading_word_0_hex: String,
|
|
pub header_flag_word_3: Option<u32>,
|
|
pub header_flag_word_3_hex: Option<String>,
|
|
pub map_path: Option<String>,
|
|
pub display_name: Option<String>,
|
|
pub profile_byte_0x77: u8,
|
|
pub profile_byte_0x77_hex: String,
|
|
pub profile_byte_0x82: u8,
|
|
pub profile_byte_0x82_hex: String,
|
|
pub profile_byte_0x97: u8,
|
|
pub profile_byte_0x97_hex: String,
|
|
pub profile_byte_0xc5: u8,
|
|
pub profile_byte_0xc5_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedCandidateAvailabilityTable {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub header_offset: usize,
|
|
pub entries_offset: usize,
|
|
pub entries_end_offset: usize,
|
|
pub observed_entry_count: usize,
|
|
pub zero_availability_count: usize,
|
|
pub zero_availability_names: Vec<String>,
|
|
pub footer_progress_hex_words: Vec<String>,
|
|
pub entries: Vec<SmpRt3105SaveNameTableEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedNamedLocomotiveAvailabilityTable {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
#[serde(default)]
|
|
pub header_offset: Option<usize>,
|
|
#[serde(default)]
|
|
pub entries_offset: Option<usize>,
|
|
#[serde(default)]
|
|
pub entries_end_offset: Option<usize>,
|
|
pub observed_entry_count: usize,
|
|
pub zero_availability_count: usize,
|
|
pub zero_availability_names: Vec<String>,
|
|
pub entries: Vec<SmpRt3105SaveNameTableEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedLocomotiveCatalogEntry {
|
|
pub locomotive_id: u32,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedLocomotiveCatalog {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
#[serde(default)]
|
|
pub entries_offset: Option<usize>,
|
|
pub observed_entry_count: usize,
|
|
pub entries: Vec<SmpLoadedLocomotiveCatalogEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedCargoCatalogEntry {
|
|
pub slot_id: u32,
|
|
pub label: String,
|
|
#[serde(default)]
|
|
pub cargo_class: RuntimeCargoClass,
|
|
pub book_index: usize,
|
|
pub max_annual_production_word: u32,
|
|
pub mode_word: u32,
|
|
pub runtime_import_branch_kind: String,
|
|
pub annual_amount_word: u32,
|
|
pub supplied_cargo_token_word: u32,
|
|
#[serde(default)]
|
|
pub supplied_cargo_token_probable_high16_ascii_stem: Option<String>,
|
|
pub demanded_cargo_token_word: u32,
|
|
#[serde(default)]
|
|
pub demanded_cargo_token_probable_high16_ascii_stem: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedCargoCatalog {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
#[serde(default)]
|
|
pub root_offset: Option<usize>,
|
|
pub observed_entry_count: usize,
|
|
pub entries: Vec<SmpLoadedCargoCatalogEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedWorldIssue37State {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub issue_value: u32,
|
|
pub issue_value_hex: String,
|
|
pub issue_38_value: u32,
|
|
pub issue_38_value_hex: String,
|
|
pub issue_39_value: u32,
|
|
pub issue_39_value_hex: String,
|
|
pub issue_3a_value: u32,
|
|
pub issue_3a_value_hex: String,
|
|
pub multiplier_raw_u32: u32,
|
|
pub multiplier_raw_hex: String,
|
|
pub multiplier_value_f32_text: String,
|
|
#[serde(default)]
|
|
pub issue_opinion_base_terms_raw_i32: Vec<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedWorldEconomicTuningState {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub mirror_raw_u32: u32,
|
|
pub mirror_raw_hex: String,
|
|
pub mirror_value_f32_text: String,
|
|
pub lane_raw_u32: Vec<u32>,
|
|
pub lane_raw_hex: Vec<String>,
|
|
pub lane_value_f32_text: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedWorldFinanceNeighborhoodState {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub packed_year_word_raw_u16: u16,
|
|
pub packed_year_word_raw_hex: String,
|
|
pub partial_year_progress_raw_u8: u8,
|
|
pub partial_year_progress_raw_hex: String,
|
|
pub current_calendar_tuple_word_raw_u32: u32,
|
|
pub current_calendar_tuple_word_raw_hex: String,
|
|
pub current_calendar_tuple_word_2_raw_u32: u32,
|
|
pub current_calendar_tuple_word_2_raw_hex: String,
|
|
pub absolute_counter_raw_u32: u32,
|
|
pub absolute_counter_raw_hex: String,
|
|
pub absolute_counter_mirror_raw_u32: u32,
|
|
pub absolute_counter_mirror_raw_hex: String,
|
|
pub stock_policy_raw_u8: u8,
|
|
pub stock_policy_raw_hex: String,
|
|
pub bond_policy_raw_u8: u8,
|
|
pub bond_policy_raw_hex: String,
|
|
pub bankruptcy_policy_raw_u8: u8,
|
|
pub bankruptcy_policy_raw_hex: String,
|
|
pub dividend_policy_raw_u8: u8,
|
|
pub dividend_policy_raw_hex: String,
|
|
pub building_density_growth_setting_raw_u32: u32,
|
|
pub building_density_growth_setting_raw_hex: String,
|
|
pub labels: Vec<String>,
|
|
pub relative_offsets: Vec<usize>,
|
|
pub relative_offset_hex: Vec<String>,
|
|
pub raw_u32: Vec<u32>,
|
|
pub raw_hex: Vec<String>,
|
|
pub value_i32: Vec<i32>,
|
|
pub value_f32_text: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedWorldLocomotivePolicyState {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
#[serde(default)]
|
|
pub selected_year_gap_scalar_raw_u32: Option<u32>,
|
|
#[serde(default)]
|
|
pub selected_year_gap_scalar_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub selected_year_gap_scalar_value_f32_text: Option<String>,
|
|
#[serde(default)]
|
|
pub linked_site_removal_follow_on_gate_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub linked_site_removal_follow_on_gate_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub auto_show_grade_during_track_lay_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub auto_show_grade_during_track_lay_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub starting_building_density_level_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub starting_building_density_level_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub building_density_growth_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub building_density_growth_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub leftover_simulation_time_accumulator_raw_u32: Option<u32>,
|
|
#[serde(default)]
|
|
pub leftover_simulation_time_accumulator_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub leftover_simulation_time_accumulator_value_f32_text: Option<String>,
|
|
#[serde(default)]
|
|
pub selected_year_lane_snapshot_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub selected_year_lane_snapshot_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub all_steam_locomotives_available_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub all_steam_locomotives_available_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub all_diesel_locomotives_available_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub all_diesel_locomotives_available_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub all_electric_locomotives_available_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub all_electric_locomotives_available_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub cached_available_locomotive_rating_raw_u32: Option<u32>,
|
|
#[serde(default)]
|
|
pub cached_available_locomotive_rating_raw_hex: Option<String>,
|
|
#[serde(default)]
|
|
pub cached_available_locomotive_rating_value_f32_text: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedCompanyRosterEntry {
|
|
pub company_id: u32,
|
|
pub active: bool,
|
|
#[serde(default)]
|
|
pub controller_kind: RuntimeCompanyControllerKind,
|
|
pub current_cash: i64,
|
|
pub debt: u64,
|
|
#[serde(default)]
|
|
pub credit_rating_score: Option<i64>,
|
|
#[serde(default)]
|
|
pub prime_rate: Option<i64>,
|
|
#[serde(default)]
|
|
pub available_track_laying_capacity: Option<u32>,
|
|
#[serde(default)]
|
|
pub track_piece_counts: RuntimeTrackPieceCounts,
|
|
#[serde(default)]
|
|
pub linked_chairman_profile_id: Option<u32>,
|
|
#[serde(default)]
|
|
pub book_value_per_share: i64,
|
|
#[serde(default)]
|
|
pub investor_confidence: i64,
|
|
#[serde(default)]
|
|
pub management_attitude: i64,
|
|
#[serde(default)]
|
|
pub takeover_cooldown_year: Option<u32>,
|
|
#[serde(default)]
|
|
pub merger_cooldown_year: Option<u32>,
|
|
#[serde(default)]
|
|
pub preferred_locomotive_engine_type_raw_u8: Option<u8>,
|
|
#[serde(default)]
|
|
pub market_state: Option<RuntimeCompanyMarketState>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedCompanyRoster {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub observed_entry_count: usize,
|
|
#[serde(default)]
|
|
pub selected_company_id: Option<u32>,
|
|
pub entries: Vec<SmpLoadedCompanyRosterEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedChairmanProfileEntry {
|
|
pub profile_id: u32,
|
|
pub name: String,
|
|
pub active: bool,
|
|
#[serde(default)]
|
|
pub current_cash: i64,
|
|
#[serde(default)]
|
|
pub linked_company_id: Option<u32>,
|
|
#[serde(default)]
|
|
pub company_holdings: BTreeMap<u32, u32>,
|
|
#[serde(default)]
|
|
pub holdings_value_total: i64,
|
|
#[serde(default)]
|
|
pub net_worth_total: i64,
|
|
#[serde(default)]
|
|
pub purchasing_power_total: i64,
|
|
#[serde(default)]
|
|
pub personality_byte_0x291: Option<u8>,
|
|
#[serde(default)]
|
|
pub issue_opinion_terms_raw_i32: Vec<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedChairmanProfileTable {
|
|
pub source_kind: String,
|
|
pub semantic_family: String,
|
|
pub observed_entry_count: usize,
|
|
#[serde(default)]
|
|
pub selected_chairman_profile_id: Option<u32>,
|
|
pub entries: Vec<SmpLoadedChairmanProfileEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveScalarCandidate {
|
|
pub relative_offset: usize,
|
|
pub relative_offset_hex: String,
|
|
pub raw_u64: u64,
|
|
pub raw_u64_hex: String,
|
|
pub value_i64: i64,
|
|
pub value_f64: f64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveDwordCandidate {
|
|
pub label: String,
|
|
pub relative_offset: usize,
|
|
pub relative_offset_hex: String,
|
|
pub raw_u32: u32,
|
|
pub raw_u32_hex: String,
|
|
pub value_i32: i32,
|
|
pub value_f32: f32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveWorldSelectionRoleAnalysisEntry {
|
|
pub slot_index: usize,
|
|
pub selector_byte: u8,
|
|
pub selector_byte_hex: String,
|
|
pub role_gate_byte: u8,
|
|
pub role_gate_byte_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpSaveWorldSelectionRoleAnalysis {
|
|
pub selected_company_id: u32,
|
|
pub selected_chairman_profile_id: u32,
|
|
pub campaign_override_flag: u8,
|
|
pub campaign_override_flag_hex: String,
|
|
pub chairman_slots: Vec<SmpSaveWorldSelectionRoleAnalysisEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveCompanyRecordAnalysisEntry {
|
|
pub company_id: u32,
|
|
pub name: String,
|
|
pub active: bool,
|
|
#[serde(default)]
|
|
pub linked_chairman_profile_id: Option<u32>,
|
|
pub outstanding_shares: u32,
|
|
pub debt: u64,
|
|
pub bond_count: u8,
|
|
#[serde(default)]
|
|
pub live_bond_slots: Vec<crate::RuntimeCompanyBondSlot>,
|
|
#[serde(default)]
|
|
pub largest_live_bond_principal: Option<u32>,
|
|
#[serde(default)]
|
|
pub highest_coupon_live_bond_principal: Option<u32>,
|
|
#[serde(default)]
|
|
pub available_track_laying_capacity: Option<u32>,
|
|
pub company_value_scalar_f32: f32,
|
|
pub cached_share_support_scalar_f32: f32,
|
|
pub cached_share_price_f32: f32,
|
|
pub chairman_salary_baseline: u32,
|
|
pub chairman_salary_current: u32,
|
|
pub chairman_bonus_year: u32,
|
|
pub chairman_bonus_amount: i32,
|
|
pub founding_year: u32,
|
|
pub last_bankruptcy_year: u32,
|
|
pub last_dividend_year: u32,
|
|
pub preferred_locomotive_engine_type_raw_u8: u8,
|
|
pub preferred_locomotive_engine_type_raw_hex: String,
|
|
pub city_connection_latch: bool,
|
|
pub linked_transit_latch: bool,
|
|
pub merger_cooldown_year: u32,
|
|
pub takeover_cooldown_year: u32,
|
|
#[serde(default)]
|
|
pub scalar_dword_candidates: Vec<SmpSaveDwordCandidate>,
|
|
#[serde(default)]
|
|
pub post_capacity_dword_candidates: Vec<SmpSaveDwordCandidate>,
|
|
#[serde(default)]
|
|
pub stat_band_root_0cfb_candidates: Vec<SmpSaveDwordCandidate>,
|
|
#[serde(default)]
|
|
pub stat_band_root_0d7f_candidates: Vec<SmpSaveDwordCandidate>,
|
|
#[serde(default)]
|
|
pub stat_band_root_1c47_candidates: Vec<SmpSaveDwordCandidate>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveChairmanRecordAnalysisEntry {
|
|
pub profile_id: u32,
|
|
pub name: String,
|
|
pub active: bool,
|
|
pub current_cash: f64,
|
|
#[serde(default)]
|
|
pub linked_company_id: Option<u32>,
|
|
#[serde(default)]
|
|
pub holdings_by_company: BTreeMap<u32, u32>,
|
|
#[serde(default)]
|
|
pub derived_holdings_share_price_total: Option<i64>,
|
|
#[serde(default)]
|
|
pub derived_net_worth_share_price_total: Option<i64>,
|
|
#[serde(default)]
|
|
pub derived_cached_purchasing_power_total: Option<i64>,
|
|
pub personality_byte_0x291: u8,
|
|
pub personality_byte_0x291_hex: String,
|
|
#[serde(default)]
|
|
pub cached_scalar_candidates: Vec<SmpSaveScalarCandidate>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpSaveCompanyChairmanAnalysisReport {
|
|
pub profile_family: String,
|
|
#[serde(default)]
|
|
pub selected_company_id: Option<u32>,
|
|
#[serde(default)]
|
|
pub selected_chairman_profile_id: Option<u32>,
|
|
#[serde(default)]
|
|
pub world_selection_context: Option<SmpSaveWorldSelectionRoleAnalysis>,
|
|
#[serde(default)]
|
|
pub world_issue_37: Option<SmpSaveWorldIssue37Probe>,
|
|
#[serde(default)]
|
|
pub world_economic_tuning: Option<SmpSaveWorldEconomicTuningProbe>,
|
|
#[serde(default)]
|
|
pub world_finance_neighborhood: Option<SmpSaveWorldFinanceNeighborhoodProbe>,
|
|
#[serde(default)]
|
|
pub train_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
|
#[serde(default)]
|
|
pub train_collection_directory: Option<SmpSaveTrainCollectionDirectoryProbe>,
|
|
#[serde(default)]
|
|
pub region_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
|
#[serde(default)]
|
|
pub region_record_triplets: Option<SmpSaveRegionRecordTripletProbe>,
|
|
#[serde(default)]
|
|
pub region_queued_notice_records: Option<SmpSaveRegionQueuedNoticeRecordProbe>,
|
|
#[serde(default)]
|
|
pub placed_structure_collection_header: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
|
#[serde(default)]
|
|
pub placed_structure_record_triplets: Option<SmpSavePlacedStructureRecordTripletProbe>,
|
|
#[serde(default)]
|
|
pub placed_structure_dynamic_side_buffer: Option<SmpSavePlacedStructureDynamicSideBufferProbe>,
|
|
#[serde(default)]
|
|
pub placed_structure_dynamic_side_buffer_alignment:
|
|
Option<SmpSavePlacedStructureDynamicSideBufferAlignmentProbe>,
|
|
#[serde(default)]
|
|
pub unclassified_tagged_collection_headers: Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe>,
|
|
#[serde(default)]
|
|
pub company_entries: Vec<SmpSaveCompanyRecordAnalysisEntry>,
|
|
#[serde(default)]
|
|
pub chairman_entries: Vec<SmpSaveChairmanRecordAnalysisEntry>,
|
|
#[serde(default)]
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpServiceTraceBranchStatus {
|
|
pub branch_name: String,
|
|
pub status: String,
|
|
#[serde(default)]
|
|
pub grounded_inputs: Vec<String>,
|
|
#[serde(default)]
|
|
pub blocking_inputs: Vec<String>,
|
|
#[serde(default)]
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPeriodicCompanyServiceTraceEntry {
|
|
pub company_id: u32,
|
|
pub name: String,
|
|
pub active: bool,
|
|
#[serde(default)]
|
|
pub linked_chairman_profile_id: Option<u32>,
|
|
pub preferred_locomotive_engine_type_raw_u8: u8,
|
|
pub city_connection_latch: bool,
|
|
pub linked_transit_latch: bool,
|
|
#[serde(default)]
|
|
pub branches: Vec<SmpServiceTraceBranchStatus>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPeriodicCompanyServiceTraceReport {
|
|
pub profile_family: String,
|
|
#[serde(default)]
|
|
pub selected_company_id: Option<u32>,
|
|
#[serde(default)]
|
|
pub world_issue_37_present: bool,
|
|
#[serde(default)]
|
|
pub world_finance_neighborhood_present: bool,
|
|
#[serde(default)]
|
|
pub region_record_body_present: bool,
|
|
#[serde(default)]
|
|
pub placed_structure_record_body_present: bool,
|
|
#[serde(default)]
|
|
pub infrastructure_asset_side_buffer_present: bool,
|
|
#[serde(default)]
|
|
pub companies: Vec<SmpPeriodicCompanyServiceTraceEntry>,
|
|
#[serde(default)]
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRegionServiceTraceEntry {
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub profile_collection_count: Option<u32>,
|
|
pub policy_leading_f32_0_text: String,
|
|
pub policy_leading_f32_1_text: String,
|
|
pub policy_leading_f32_2_text: String,
|
|
#[serde(default)]
|
|
pub policy_reserved_dword_hex_words: Vec<String>,
|
|
pub policy_trailing_word_hex: String,
|
|
#[serde(default)]
|
|
pub branches: Vec<SmpServiceTraceBranchStatus>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpRegionServiceTraceReport {
|
|
pub profile_family: String,
|
|
#[serde(default)]
|
|
pub region_collection_header_present: bool,
|
|
#[serde(default)]
|
|
pub region_record_triplet_count: usize,
|
|
#[serde(default)]
|
|
pub queued_notice_record_count: usize,
|
|
#[serde(default)]
|
|
pub atlas_candidate_consumers: Vec<String>,
|
|
#[serde(default)]
|
|
pub known_owner_bridge_fields: Vec<String>,
|
|
#[serde(default)]
|
|
pub known_bridge_helpers: Vec<String>,
|
|
#[serde(default)]
|
|
pub next_owner_questions: Vec<String>,
|
|
#[serde(default)]
|
|
pub candidate_consumer_hypotheses: Vec<SmpServiceConsumerHypothesis>,
|
|
#[serde(default)]
|
|
pub entries: Vec<SmpRegionServiceTraceEntry>,
|
|
#[serde(default)]
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpInfrastructureAssetTraceReport {
|
|
pub profile_family: String,
|
|
#[serde(default)]
|
|
pub placed_structure_collection_header_present: bool,
|
|
#[serde(default)]
|
|
pub placed_structure_record_triplet_count: usize,
|
|
#[serde(default)]
|
|
pub side_buffer_present: bool,
|
|
#[serde(default)]
|
|
pub side_buffer_decoded_embedded_name_row_count: usize,
|
|
#[serde(default)]
|
|
pub side_buffer_unique_name_pair_count: usize,
|
|
#[serde(default)]
|
|
pub bridge_like_name_pair_count: usize,
|
|
#[serde(default)]
|
|
pub tunnel_like_name_pair_count: usize,
|
|
#[serde(default)]
|
|
pub track_cap_like_name_pair_count: usize,
|
|
#[serde(default)]
|
|
pub triplet_alignment_overlap_count: usize,
|
|
#[serde(default)]
|
|
pub atlas_candidate_consumers: Vec<String>,
|
|
#[serde(default)]
|
|
pub known_owner_bridge_fields: Vec<String>,
|
|
#[serde(default)]
|
|
pub known_bridge_helpers: Vec<String>,
|
|
#[serde(default)]
|
|
pub next_owner_questions: Vec<String>,
|
|
#[serde(default)]
|
|
pub candidate_consumer_hypotheses: Vec<SmpServiceConsumerHypothesis>,
|
|
#[serde(default)]
|
|
pub branches: Vec<SmpServiceTraceBranchStatus>,
|
|
#[serde(default)]
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpServiceConsumerHypothesis {
|
|
pub label: String,
|
|
pub status: String,
|
|
#[serde(default)]
|
|
pub candidate_consumers: Vec<String>,
|
|
#[serde(default)]
|
|
pub evidence: Vec<String>,
|
|
#[serde(default)]
|
|
pub blockers: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedSpecialConditionsTable {
|
|
pub source_kind: String,
|
|
pub table_offset: usize,
|
|
pub table_len: usize,
|
|
pub enabled_visible_count: usize,
|
|
pub enabled_visible_labels: Vec<String>,
|
|
pub entries: Vec<SmpSpecialConditionEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedEventRuntimeCollectionSummary {
|
|
pub source_kind: String,
|
|
pub mechanism_family: String,
|
|
pub mechanism_confidence: String,
|
|
#[serde(default)]
|
|
pub container_profile_family: Option<String>,
|
|
pub metadata_tag_offset: usize,
|
|
pub records_tag_offset: usize,
|
|
pub close_tag_offset: usize,
|
|
pub packed_state_version: u32,
|
|
pub packed_state_version_hex: String,
|
|
pub live_id_bound: u32,
|
|
pub live_record_count: usize,
|
|
pub live_entry_ids: Vec<u32>,
|
|
#[serde(default)]
|
|
pub decoded_record_count: usize,
|
|
#[serde(default)]
|
|
pub imported_runtime_record_count: usize,
|
|
#[serde(default)]
|
|
pub records: Vec<SmpLoadedPackedEventRecordSummary>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedPackedEventRecordSummary {
|
|
pub record_index: usize,
|
|
pub live_entry_id: u32,
|
|
#[serde(default)]
|
|
pub payload_offset: Option<usize>,
|
|
#[serde(default)]
|
|
pub payload_len: Option<usize>,
|
|
pub decode_status: String,
|
|
#[serde(default)]
|
|
pub payload_family: String,
|
|
#[serde(default)]
|
|
pub trigger_kind: Option<u8>,
|
|
#[serde(default)]
|
|
pub active: Option<bool>,
|
|
#[serde(default)]
|
|
pub marks_collection_dirty: Option<bool>,
|
|
#[serde(default)]
|
|
pub one_shot: Option<bool>,
|
|
#[serde(default)]
|
|
pub compact_control: Option<SmpLoadedPackedEventCompactControlSummary>,
|
|
#[serde(default)]
|
|
pub text_bands: Vec<SmpLoadedPackedEventTextBandSummary>,
|
|
#[serde(default)]
|
|
pub standalone_condition_row_count: usize,
|
|
#[serde(default)]
|
|
pub standalone_condition_rows: Vec<SmpLoadedPackedEventConditionRowSummary>,
|
|
#[serde(default)]
|
|
pub negative_sentinel_scope: Option<SmpLoadedPackedEventNegativeSentinelScopeSummary>,
|
|
#[serde(default)]
|
|
pub grouped_effect_row_counts: Vec<usize>,
|
|
#[serde(default)]
|
|
pub grouped_effect_rows: Vec<SmpLoadedPackedEventGroupedEffectRowSummary>,
|
|
#[serde(default)]
|
|
pub decoded_conditions: Vec<RuntimeCondition>,
|
|
#[serde(default)]
|
|
pub decoded_actions: Vec<RuntimeEffect>,
|
|
#[serde(default)]
|
|
pub executable_import_ready: bool,
|
|
#[serde(default)]
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedPackedEventNegativeSentinelScopeSummary {
|
|
pub company_test_scope: RuntimeCompanyConditionTestScope,
|
|
pub player_test_scope: RuntimePlayerConditionTestScope,
|
|
pub territory_scope_selector_is_0x63: bool,
|
|
#[serde(default)]
|
|
pub source_row_indexes: Vec<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedPackedEventCompactControlSummary {
|
|
pub mode_byte_0x7ef: u8,
|
|
pub primary_selector_0x7f0: u32,
|
|
pub grouped_mode_0x7f4: u8,
|
|
pub one_shot_header_0x7f5: u32,
|
|
pub modifier_flag_0x7f9: u8,
|
|
pub modifier_flag_0x7fa: u8,
|
|
pub grouped_target_scope_ordinals_0x7fb: Vec<u8>,
|
|
pub grouped_scope_checkboxes_0x7ff: Vec<u8>,
|
|
pub summary_toggle_0x800: u8,
|
|
pub grouped_territory_selectors_0x80f: Vec<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedPackedEventTextBandSummary {
|
|
pub label: String,
|
|
pub packed_len: usize,
|
|
pub present: bool,
|
|
pub preview: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedPackedEventConditionRowSummary {
|
|
pub row_index: usize,
|
|
pub raw_condition_id: i32,
|
|
pub subtype: u8,
|
|
#[serde(default)]
|
|
pub flag_bytes: Vec<u8>,
|
|
#[serde(default)]
|
|
pub candidate_name: Option<String>,
|
|
#[serde(default)]
|
|
pub comparator: Option<String>,
|
|
#[serde(default)]
|
|
pub metric: Option<String>,
|
|
#[serde(default)]
|
|
pub semantic_family: Option<String>,
|
|
#[serde(default)]
|
|
pub semantic_preview: Option<String>,
|
|
#[serde(default)]
|
|
pub recovered_cargo_slot: Option<u32>,
|
|
#[serde(default)]
|
|
pub recovered_cargo_class: Option<String>,
|
|
#[serde(default)]
|
|
pub requires_candidate_name_binding: bool,
|
|
#[serde(default)]
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
|
|
pub group_index: usize,
|
|
pub row_index: usize,
|
|
pub descriptor_id: u32,
|
|
#[serde(default)]
|
|
pub descriptor_label: Option<String>,
|
|
#[serde(default)]
|
|
pub target_mask_bits: Option<u8>,
|
|
#[serde(default)]
|
|
pub parameter_family: Option<String>,
|
|
#[serde(default)]
|
|
pub grouped_target_subject: Option<String>,
|
|
#[serde(default)]
|
|
pub grouped_target_scope: Option<String>,
|
|
pub opcode: u8,
|
|
pub raw_scalar_value: i32,
|
|
pub value_byte_0x09: u8,
|
|
pub value_dword_0x0d: u32,
|
|
pub value_byte_0x11: u8,
|
|
pub value_byte_0x12: u8,
|
|
pub value_word_0x14: u16,
|
|
pub value_word_0x16: u16,
|
|
pub row_shape: String,
|
|
#[serde(default)]
|
|
pub semantic_family: Option<String>,
|
|
#[serde(default)]
|
|
pub semantic_preview: Option<String>,
|
|
#[serde(default)]
|
|
pub recovered_cargo_slot: Option<u32>,
|
|
#[serde(default)]
|
|
pub recovered_cargo_class: Option<String>,
|
|
#[serde(default)]
|
|
pub recovered_cargo_label: Option<String>,
|
|
#[serde(default)]
|
|
pub recovered_locomotive_id: Option<u32>,
|
|
#[serde(default)]
|
|
pub locomotive_name: Option<String>,
|
|
#[serde(default)]
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RealGroupedTargetSubject {
|
|
Company,
|
|
Player,
|
|
Chairman,
|
|
Territory,
|
|
WholeGame,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpLoadedSaveSlice {
|
|
pub file_extension_hint: Option<String>,
|
|
pub container_profile_family: Option<String>,
|
|
pub mechanism_family: String,
|
|
pub mechanism_confidence: String,
|
|
pub trailer_family: Option<String>,
|
|
pub bridge_family: Option<String>,
|
|
pub profile: Option<SmpLoadedProfile>,
|
|
pub candidate_availability_table: Option<SmpLoadedCandidateAvailabilityTable>,
|
|
pub named_locomotive_availability_table: Option<SmpLoadedNamedLocomotiveAvailabilityTable>,
|
|
#[serde(default)]
|
|
pub locomotive_catalog: Option<SmpLoadedLocomotiveCatalog>,
|
|
#[serde(default)]
|
|
pub cargo_catalog: Option<SmpLoadedCargoCatalog>,
|
|
#[serde(default)]
|
|
pub world_issue_37_state: Option<SmpLoadedWorldIssue37State>,
|
|
#[serde(default)]
|
|
pub world_economic_tuning_state: Option<SmpLoadedWorldEconomicTuningState>,
|
|
#[serde(default)]
|
|
pub world_finance_neighborhood_state: Option<SmpLoadedWorldFinanceNeighborhoodState>,
|
|
#[serde(default)]
|
|
pub world_locomotive_policy_state: Option<SmpLoadedWorldLocomotivePolicyState>,
|
|
#[serde(default)]
|
|
pub company_roster: Option<SmpLoadedCompanyRoster>,
|
|
#[serde(default)]
|
|
pub chairman_profile_table: Option<SmpLoadedChairmanProfileTable>,
|
|
pub special_conditions_table: Option<SmpLoadedSpecialConditionsTable>,
|
|
pub event_runtime_collection: Option<SmpLoadedEventRuntimeCollectionSummary>,
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SmpPackedProfileWordLane {
|
|
pub relative_offset: usize,
|
|
pub relative_offset_hex: String,
|
|
pub value: u32,
|
|
pub value_hex: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SmpInspectionReport {
|
|
pub inspection_mode: String,
|
|
pub file_extension_hint: Option<String>,
|
|
pub file_size: usize,
|
|
pub sha256: String,
|
|
pub preamble: SmpPreamble,
|
|
pub shared_header: Option<SmpSharedHeader>,
|
|
pub header_variant_probe: Option<SmpHeaderVariantProbe>,
|
|
pub first_ascii_run: Option<SmpAsciiPreview>,
|
|
pub early_content_probe: Option<SmpEarlyContentProbe>,
|
|
pub secondary_variant_probe: Option<SmpSecondaryVariantProbe>,
|
|
pub container_profile: Option<SmpContainerProfile>,
|
|
pub save_bootstrap_block: Option<SmpSaveBootstrapBlock>,
|
|
pub save_anchor_run_block: Option<SmpSaveAnchorRunBlock>,
|
|
pub runtime_anchor_cycle_block: Option<SmpRuntimeAnchorCycleBlock>,
|
|
pub runtime_trailer_block: Option<SmpRuntimeTrailerBlock>,
|
|
pub runtime_post_span_probe: Option<SmpRuntimePostSpanProbe>,
|
|
pub rt3_105_post_span_bridge_probe: Option<SmpRt3105PostSpanBridgeProbe>,
|
|
pub rt3_105_save_bridge_payload_probe: Option<SmpRt3105SaveBridgePayloadProbe>,
|
|
pub save_world_selection_context_probe: Option<SmpSaveWorldSelectionContextProbe>,
|
|
pub save_world_issue_37_probe: Option<SmpSaveWorldIssue37Probe>,
|
|
pub save_world_economic_tuning_probe: Option<SmpSaveWorldEconomicTuningProbe>,
|
|
pub save_world_finance_neighborhood_probe: Option<SmpSaveWorldFinanceNeighborhoodProbe>,
|
|
pub save_company_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
|
pub save_chairman_profile_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
|
pub save_train_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
|
pub save_train_collection_directory_probe: Option<SmpSaveTrainCollectionDirectoryProbe>,
|
|
pub save_region_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
|
pub save_region_record_triplet_probe: Option<SmpSaveRegionRecordTripletProbe>,
|
|
#[serde(default)]
|
|
pub save_region_queued_notice_record_probe: Option<SmpSaveRegionQueuedNoticeRecordProbe>,
|
|
pub save_placed_structure_collection_header_probe: Option<SmpSaveTaggedCollectionHeaderProbe>,
|
|
pub save_placed_structure_record_triplet_probe:
|
|
Option<SmpSavePlacedStructureRecordTripletProbe>,
|
|
#[serde(default)]
|
|
pub save_placed_structure_dynamic_side_buffer_probe:
|
|
Option<SmpSavePlacedStructureDynamicSideBufferProbe>,
|
|
#[serde(default)]
|
|
pub save_unclassified_tagged_collection_header_probes:
|
|
Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe>,
|
|
#[serde(default)]
|
|
pub save_company_roster_probe: Option<SmpLoadedCompanyRoster>,
|
|
#[serde(default)]
|
|
pub save_chairman_profile_table_probe: Option<SmpLoadedChairmanProfileTable>,
|
|
pub rt3_105_save_name_table_probe: Option<SmpRt3105SaveNameTableProbe>,
|
|
pub rt3_105_save_named_locomotive_availability_probe:
|
|
Option<SmpRt3105SaveNamedLocomotiveAvailabilityProbe>,
|
|
pub special_conditions_probe: Option<SmpSpecialConditionsProbe>,
|
|
pub smp_aligned_runtime_rule_band_probe: Option<SmpAlignedRuntimeRuleBandProbe>,
|
|
pub post_special_conditions_scalar_probe: Option<SmpPostSpecialConditionsScalarProbe>,
|
|
pub post_text_field_neighborhood_probe: Option<SmpPostTextFieldNeighborhoodProbe>,
|
|
pub locomotive_policy_neighborhood_probe: Option<SmpLocomotivePolicyNeighborhoodProbe>,
|
|
pub pre_recipe_scalar_plateau_probe: Option<SmpPreRecipeScalarPlateauProbe>,
|
|
pub recipe_book_summary_probe: Option<SmpRecipeBookSummaryProbe>,
|
|
pub classic_rehydrate_profile_probe: Option<SmpClassicRehydrateProfileProbe>,
|
|
pub rt3_105_packed_profile_probe: Option<SmpRt3105PackedProfileProbe>,
|
|
pub save_load_summary: Option<SmpSaveLoadSummary>,
|
|
pub event_runtime_collection_summary: Option<SmpLoadedEventRuntimeCollectionSummary>,
|
|
pub contains_grounded_runtime_tags: bool,
|
|
pub known_tag_hits: Vec<SmpKnownTagHit>,
|
|
pub notes: Vec<String>,
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
pub fn inspect_smp_file(path: &Path) -> Result<SmpInspectionReport, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(path)?;
|
|
Ok(inspect_bundle_bytes(
|
|
&bytes,
|
|
path.extension()
|
|
.and_then(|extension| extension.to_str())
|
|
.map(|extension| extension.to_ascii_lowercase()),
|
|
))
|
|
}
|
|
|
|
pub fn inspect_unclassified_save_collection_headers_file(
|
|
path: &Path,
|
|
) -> Result<Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe>, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(path)?;
|
|
let file_extension_hint = path
|
|
.extension()
|
|
.and_then(|extension| extension.to_str())
|
|
.map(|extension| extension.to_ascii_lowercase());
|
|
let shared_header = parse_shared_header(&bytes);
|
|
let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe);
|
|
let first_ascii_run = find_first_ascii_run(&bytes);
|
|
let early_content_probe = first_ascii_run
|
|
.as_ref()
|
|
.and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run));
|
|
let secondary_variant_probe = early_content_probe
|
|
.as_ref()
|
|
.and_then(classify_secondary_variant_probe);
|
|
let container_profile = classify_container_profile(
|
|
file_extension_hint.as_deref(),
|
|
header_variant_probe.as_ref(),
|
|
secondary_variant_probe.as_ref(),
|
|
);
|
|
let save_company_collection_header_probe = parse_save_company_collection_header_probe(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_chairman_profile_collection_header_probe =
|
|
parse_save_chairman_profile_collection_header_probe(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_train_collection_header_probe = parse_save_train_collection_header_probe(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_region_collection_header_probe = parse_save_region_collection_header_probe(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_placed_structure_collection_header_probe =
|
|
parse_save_placed_structure_collection_header_probe(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let known_header_probes = [
|
|
save_company_collection_header_probe.as_ref(),
|
|
save_chairman_profile_collection_header_probe.as_ref(),
|
|
save_train_collection_header_probe.as_ref(),
|
|
save_region_collection_header_probe.as_ref(),
|
|
save_placed_structure_collection_header_probe.as_ref(),
|
|
];
|
|
let probes = scan_save_unclassified_tagged_collection_header_probes(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
Ok(
|
|
filter_unclassified_tagged_collection_header_probes_outside_known_spans(
|
|
probes,
|
|
&known_header_probes,
|
|
),
|
|
)
|
|
}
|
|
|
|
pub fn inspect_save_placed_structure_dynamic_side_buffer_file(
|
|
path: &Path,
|
|
) -> Result<Option<SmpSavePlacedStructureDynamicSideBufferProbe>, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(path)?;
|
|
let file_extension_hint = path
|
|
.extension()
|
|
.and_then(|extension| extension.to_str())
|
|
.map(|extension| extension.to_ascii_lowercase());
|
|
let shared_header = parse_shared_header(&bytes);
|
|
let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe);
|
|
let first_ascii_run = find_first_ascii_run(&bytes);
|
|
let early_content_probe = first_ascii_run
|
|
.as_ref()
|
|
.and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run));
|
|
let secondary_variant_probe = early_content_probe
|
|
.as_ref()
|
|
.and_then(classify_secondary_variant_probe);
|
|
let container_profile = classify_container_profile(
|
|
file_extension_hint.as_deref(),
|
|
header_variant_probe.as_ref(),
|
|
secondary_variant_probe.as_ref(),
|
|
);
|
|
Ok(parse_save_placed_structure_dynamic_side_buffer_probe(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
))
|
|
}
|
|
|
|
pub fn inspect_save_region_queued_notice_records_file(
|
|
path: &Path,
|
|
) -> Result<Option<SmpSaveRegionQueuedNoticeRecordProbe>, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(path)?;
|
|
let file_extension_hint = path
|
|
.extension()
|
|
.and_then(|extension| extension.to_str())
|
|
.map(|extension| extension.to_ascii_lowercase());
|
|
let shared_header = parse_shared_header(&bytes);
|
|
let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe);
|
|
let first_ascii_run = find_first_ascii_run(&bytes);
|
|
let early_content_probe = first_ascii_run
|
|
.as_ref()
|
|
.and_then(|ascii_run| probe_early_content_layout(&bytes, ascii_run));
|
|
let secondary_variant_probe = early_content_probe
|
|
.as_ref()
|
|
.and_then(classify_secondary_variant_probe);
|
|
let container_profile = classify_container_profile(
|
|
file_extension_hint.as_deref(),
|
|
header_variant_probe.as_ref(),
|
|
secondary_variant_probe.as_ref(),
|
|
);
|
|
let save_region_collection_header_probe = parse_save_region_collection_header_probe(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
Ok(parse_save_region_queued_notice_record_probe(
|
|
&bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
save_region_collection_header_probe.as_ref(),
|
|
))
|
|
}
|
|
|
|
fn build_service_trace_branch_status(
|
|
branch_name: &str,
|
|
status: &str,
|
|
grounded_inputs: &[&str],
|
|
blocking_inputs: &[&str],
|
|
notes: &[&str],
|
|
) -> SmpServiceTraceBranchStatus {
|
|
SmpServiceTraceBranchStatus {
|
|
branch_name: branch_name.to_string(),
|
|
status: status.to_string(),
|
|
grounded_inputs: grounded_inputs
|
|
.iter()
|
|
.map(|value| value.to_string())
|
|
.collect(),
|
|
blocking_inputs: blocking_inputs
|
|
.iter()
|
|
.map(|value| value.to_string())
|
|
.collect(),
|
|
notes: notes.iter().map(|value| value.to_string()).collect(),
|
|
}
|
|
}
|
|
|
|
pub fn inspect_save_periodic_company_service_trace_file(
|
|
path: &Path,
|
|
) -> Result<SmpPeriodicCompanyServiceTraceReport, Box<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(path)?;
|
|
let analysis = inspect_save_company_and_chairman_analysis_file(path)?;
|
|
let profile_family = analysis.profile_family.clone();
|
|
let selected_company_id = analysis.selected_company_id;
|
|
let region_record_body_present = analysis.region_record_triplets.is_some();
|
|
let placed_structure_record_body_present = analysis.placed_structure_record_triplets.is_some();
|
|
let infrastructure_asset_side_buffer_present =
|
|
analysis.placed_structure_dynamic_side_buffer.is_some();
|
|
let world_issue_37_present = analysis.world_issue_37.is_some();
|
|
let world_finance_neighborhood_present = analysis.world_finance_neighborhood.is_some();
|
|
|
|
let companies = analysis
|
|
.company_entries
|
|
.iter()
|
|
.map(|entry| {
|
|
let mut branches = Vec::new();
|
|
branches.push(build_service_trace_branch_status(
|
|
"route_preference_override",
|
|
if entry.preferred_locomotive_engine_type_raw_u8 == 2 {
|
|
"grounded_electric_override_candidate"
|
|
} else {
|
|
"grounded_non_electric_or_inactive_override_candidate"
|
|
},
|
|
&[
|
|
"company periodic side-latch trio",
|
|
"world route-preference byte",
|
|
"preferred locomotive engine-type lane",
|
|
],
|
|
&[],
|
|
&[
|
|
"This probe keeps the outer owner at the save-owned input level; the concrete runtime reader/apply/restore seam is already grounded separately.",
|
|
],
|
|
));
|
|
branches.push(build_service_trace_branch_status(
|
|
"annual_finance_policy",
|
|
"runnable_from_grounded_owner_state",
|
|
&[
|
|
"company market/cache owner state",
|
|
"periodic side-latches",
|
|
"world issue/timing owner state",
|
|
"derived annual-finance readers",
|
|
],
|
|
&[],
|
|
&[
|
|
"The shellless annual-finance helper is already rehosted on top of runtime-owned state.",
|
|
],
|
|
));
|
|
branches.push(build_service_trace_branch_status(
|
|
"city_connection_announcement",
|
|
"blocked_missing_region_and_infrastructure_asset_owner_seams",
|
|
&[
|
|
"company periodic side-latches",
|
|
"route-preference override seam",
|
|
"annual-finance sequencing owner",
|
|
],
|
|
&[
|
|
"region pending/completion/one-shot/severity lanes",
|
|
"stable region id or class discriminator",
|
|
"placed-structure or infrastructure-asset consumer mapping",
|
|
],
|
|
&[
|
|
"Current atlas evidence places this branch above both the region pending-bonus lane and infrastructure/placed-structure consumers.",
|
|
],
|
|
));
|
|
branches.push(build_service_trace_branch_status(
|
|
"linked_transit_roster_maintenance",
|
|
"blocked_missing_infrastructure_asset_consumer_mapping",
|
|
&[
|
|
"company linked-transit latch",
|
|
"route-preference override seam",
|
|
],
|
|
&[
|
|
"placed-structure record-body semantics",
|
|
"0x38a5 infrastructure-asset consumer mapping",
|
|
],
|
|
&[
|
|
"The save side now grounds the owner seams, but not yet the higher-layer consumer that turns them into roster or route actions.",
|
|
],
|
|
));
|
|
branches.push(build_service_trace_branch_status(
|
|
"industry_acquisition_side_branch",
|
|
"blocked_missing_near-city_owner_mapping",
|
|
&[
|
|
"periodic service outer owner",
|
|
"annual-finance ordering",
|
|
],
|
|
&[
|
|
"near-city industry candidate owner seam",
|
|
"city or region peer linkage",
|
|
],
|
|
&[
|
|
"The outer owner is bounded, but the concrete candidate/peer scan is not yet rehosted.",
|
|
],
|
|
));
|
|
SmpPeriodicCompanyServiceTraceEntry {
|
|
company_id: entry.company_id,
|
|
name: entry.name.clone(),
|
|
active: entry.active,
|
|
linked_chairman_profile_id: entry.linked_chairman_profile_id,
|
|
preferred_locomotive_engine_type_raw_u8: entry
|
|
.preferred_locomotive_engine_type_raw_u8,
|
|
city_connection_latch: entry.city_connection_latch,
|
|
linked_transit_latch: entry.linked_transit_latch,
|
|
branches,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut notes = Vec::new();
|
|
let _ = inspection;
|
|
notes.push(
|
|
"Periodic company service trace is intentionally an outer-owner probe: it reports save-owned branch inputs and blocker seams without serializing the full projected runtime reader state.".to_string(),
|
|
);
|
|
if region_record_body_present || placed_structure_record_body_present {
|
|
notes.push(
|
|
"The current blockers are no longer collection identity; they are missing higher-layer consumer semantics for the region and infrastructure/placed-structure owner seams.".to_string(),
|
|
);
|
|
}
|
|
|
|
Ok(SmpPeriodicCompanyServiceTraceReport {
|
|
profile_family,
|
|
selected_company_id,
|
|
world_issue_37_present,
|
|
world_finance_neighborhood_present,
|
|
region_record_body_present,
|
|
placed_structure_record_body_present,
|
|
infrastructure_asset_side_buffer_present,
|
|
companies,
|
|
notes,
|
|
})
|
|
}
|
|
|
|
pub fn inspect_save_region_service_trace_file(
|
|
path: &Path,
|
|
) -> Result<SmpRegionServiceTraceReport, Box<dyn std::error::Error>> {
|
|
let analysis = inspect_save_company_and_chairman_analysis_file(path)?;
|
|
Ok(build_region_service_trace_report(&analysis))
|
|
}
|
|
|
|
pub fn inspect_save_infrastructure_asset_trace_file(
|
|
path: &Path,
|
|
) -> Result<SmpInfrastructureAssetTraceReport, Box<dyn std::error::Error>> {
|
|
let analysis = inspect_save_company_and_chairman_analysis_file(path)?;
|
|
Ok(build_infrastructure_asset_trace_report(&analysis))
|
|
}
|
|
|
|
fn build_region_service_trace_report(
|
|
analysis: &SmpSaveCompanyChairmanAnalysisReport,
|
|
) -> SmpRegionServiceTraceReport {
|
|
let atlas_candidate_consumers = vec![
|
|
"0x00422100 periodic class-0 region picker and queue seed owner".to_string(),
|
|
"0x004337c0 queued 0x20-byte notice-node append helper".to_string(),
|
|
"0x00437c00 queued-kind dispatch owner".to_string(),
|
|
"0x004c7520 kind-7 region-focused custom-modal owner".to_string(),
|
|
"0x004358d0 pending region bonus service owner".to_string(),
|
|
"0x00438710 recurring queued-notice service owner".to_string(),
|
|
"0x00420030 / 0x00420280 city-connection peer probes".to_string(),
|
|
"0x0047efe0 placed-structure linked-company resolver".to_string(),
|
|
];
|
|
let known_owner_bridge_fields = vec![
|
|
"[region+0x25e] pending-bonus severity/source lane".to_string(),
|
|
"[region+0x276] pending bonus amount".to_string(),
|
|
"[region+0x302] completion latch".to_string(),
|
|
"[region+0x316] one-shot fallback notice latch".to_string(),
|
|
"[region+0x356] localized region name".to_string(),
|
|
"[region+0x23a] world-scalar-backed region lane used in notices".to_string(),
|
|
];
|
|
let known_bridge_helpers = vec![
|
|
"0x004207d0 city_site_format_connection_bonus_status_label".to_string(),
|
|
"0x00420030 city_connection_bonus_exists_matching_peer_site".to_string(),
|
|
"0x00420280 city_connection_bonus_select_first_matching_peer_site".to_string(),
|
|
"0x0047efe0 placed_structure_query_linked_company_id".to_string(),
|
|
"0x00480bb0 placed_structure_refresh_linked_site_display_name_and_route_anchor".to_string(),
|
|
"0x00420650 city-site local scalar refresh release-side companion".to_string(),
|
|
"0x00421510 world_region_collection_refresh_records_from_tagged_bundle".to_string(),
|
|
"0x0041f5c0 world_region_load_tagged_payload_and_profile_collection_0x37f".to_string(),
|
|
"0x00455fc0 shared region tagged-payload reload companion".to_string(),
|
|
"0x00455870 region triplet-band tagged restore callback (world-region vtable +0x48)"
|
|
.to_string(),
|
|
"0x00455930 region triplet-band tagged serializer callback (world-region vtable +0x4c)"
|
|
.to_string(),
|
|
];
|
|
let next_owner_questions = vec![
|
|
"Which persisted owner seam rebuilds or restores [region+0x25e/+0x276/+0x302/+0x316]?".to_string(),
|
|
"Which stable region id or class discriminator survives save/load strongly enough to drive 0x004358d0?".to_string(),
|
|
"How far can 0x00420030/0x00420280 plus 0x0047efe0 be reused directly before the transient queued-notice family matters again?".to_string(),
|
|
];
|
|
let candidate_consumer_hypotheses = vec![
|
|
SmpServiceConsumerHypothesis {
|
|
label: "pending region bonus service path".to_string(),
|
|
status: if analysis.region_record_triplets.is_some() {
|
|
"highest_priority_static_mapping_target".to_string()
|
|
} else {
|
|
"possible_consumer_family".to_string()
|
|
},
|
|
candidate_consumers: vec![
|
|
"0x004358d0 pending region bonus service owner".to_string(),
|
|
"0x00420030 / 0x00420280 city-connection peer probes".to_string(),
|
|
"0x0047efe0 placed-structure linked-company resolver".to_string(),
|
|
],
|
|
evidence: vec![
|
|
"atlas already bounds this owner as the direct consumer of [region+0x276], [region+0x302], and [region+0x316]".to_string(),
|
|
"the new region trace already proves the record envelope and profile subcollection, so the remaining gap is the separate persisted latch seam rather than the service owner".to_string(),
|
|
"direct disassembly now shows 0x004358d0 calling 0x00420030 twice plus 0x00420280, resolving the linked company through 0x0047efe0, posting company stat slot 4, and then clearing [region+0x276] while stamping [region+0x302] or [region+0x316]".to_string(),
|
|
"the checked-in constructor owner 0x00421200 now also proves these latches are initialized locally at record construction time, which narrows the remaining gap to post-construction restore or rebuild rather than basic field identity".to_string(),
|
|
],
|
|
blockers: vec![
|
|
"persisted owner seam for [region+0x25e/+0x276/+0x302/+0x316]".to_string(),
|
|
"stable region id or class discriminator".to_string(),
|
|
],
|
|
},
|
|
SmpServiceConsumerHypothesis {
|
|
label: "region tagged-load restore path".to_string(),
|
|
status: if analysis.region_record_triplets.is_some() {
|
|
"parallel_static_mapping_target".to_string()
|
|
} else {
|
|
"possible_consumer_family".to_string()
|
|
},
|
|
candidate_consumers: vec![
|
|
"0x00421510 world_region_collection_refresh_records_from_tagged_bundle".to_string(),
|
|
"0x0041f5c0 world_region_load_tagged_payload_and_profile_collection_0x37f".to_string(),
|
|
"0x00455fc0 shared region tagged-payload reload companion".to_string(),
|
|
],
|
|
evidence: vec![
|
|
"the checked-in function map already grounds 0x00421510 as the tagged region-collection load owner that dispatches each live record through vtable slot +0x40".to_string(),
|
|
"the checked-in function map already grounds 0x0041f5c0 as the per-record load slot that reloads the tagged payload through 0x00455fc0 and then rebuilds profile collection [region+0x37f]".to_string(),
|
|
"constructor-side evidence now proves the latches are initialized locally, so the remaining gap can legitimately be framed as post-construction restore or rebuild".to_string(),
|
|
"direct disassembly of 0x0041f590/0x0041f5b0 now proves the world-region vtable root is 0x005c9a28, so the 0x00455fc0 dispatch at slot +0x48 lands on 0x00455870 and the serializer sibling at +0x4c lands on 0x00455930".to_string(),
|
|
"direct disassembly of 0x00455870/0x00455930 now shows that callback pair only restores and serializes two triplet-like three-lane scalar bands through 0x531150/0x531030 plus 0x530720/0x52e8b0, not [region+0x276/+0x302/+0x316]".to_string(),
|
|
],
|
|
blockers: vec![
|
|
"which later restore or rebuild owner rehydrates [region+0x276/+0x302/+0x316] after the shared payload loader and triplet-band callback complete".to_string(),
|
|
"whether [region+0x25e] severity/source and any stable region id/class discriminator are serialized elsewhere in the tagged region body or recomputed immediately post-load".to_string(),
|
|
],
|
|
},
|
|
SmpServiceConsumerHypothesis {
|
|
label: "periodic producer and queued-notice path".to_string(),
|
|
status: "secondary_candidate_after_pending_service".to_string(),
|
|
candidate_consumers: vec![
|
|
"0x00422100 periodic class-0 region picker and queue seed owner".to_string(),
|
|
"0x004337c0 queued 0x20-byte notice-node append helper".to_string(),
|
|
"0x00438710 recurring queued-notice service owner".to_string(),
|
|
],
|
|
evidence: vec![
|
|
"atlas ties these owners to the transient kind-7 queue family rooted at [world+0x66a6]".to_string(),
|
|
"grounded save probes now show that the ordinary-save queue family is not obviously persisted, so this looks more like runtime rebuild state than a direct save seam".to_string(),
|
|
],
|
|
blockers: vec![
|
|
"transient queue is not obviously persisted in ordinary saves".to_string(),
|
|
"needs one upstream persisted latch or rebuild owner first".to_string(),
|
|
],
|
|
},
|
|
SmpServiceConsumerHypothesis {
|
|
label: "queued kind-7 modal dispatch path".to_string(),
|
|
status: "shell_adjacent_reference_only".to_string(),
|
|
candidate_consumers: vec![
|
|
"0x00437c00 queued-kind dispatch owner".to_string(),
|
|
"0x004c7520 kind-7 region-focused custom-modal owner".to_string(),
|
|
],
|
|
evidence: vec![
|
|
"atlas already bounds this family as the shell-facing modal dispatch above the queued region id".to_string(),
|
|
"it is still useful as a reference owner for field identity, but not the first shellless rehost target".to_string(),
|
|
],
|
|
blockers: vec![
|
|
"full shell/dialog ownership remains out of scope".to_string(),
|
|
],
|
|
},
|
|
];
|
|
let entries = analysis
|
|
.region_record_triplets
|
|
.as_ref()
|
|
.map(|probe| {
|
|
probe.entries
|
|
.iter()
|
|
.map(|entry| SmpRegionServiceTraceEntry {
|
|
name: entry.name.clone(),
|
|
profile_collection_count: entry
|
|
.profile_collection
|
|
.as_ref()
|
|
.map(|collection| collection.live_record_count),
|
|
policy_leading_f32_0_text: format!("{:.6}", entry.policy_leading_f32_0),
|
|
policy_leading_f32_1_text: format!("{:.6}", entry.policy_leading_f32_1),
|
|
policy_leading_f32_2_text: format!("{:.6}", entry.policy_leading_f32_2),
|
|
policy_reserved_dword_hex_words: entry
|
|
.policy_reserved_dwords
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(),
|
|
branches: vec![
|
|
build_service_trace_branch_status(
|
|
"pending_bonus_queue_seed",
|
|
"blocked_missing_pending_bonus_owner_lane",
|
|
&[
|
|
"region triplet envelope",
|
|
"embedded profile subcollection",
|
|
"policy float lanes",
|
|
],
|
|
&[
|
|
"[region+0x276] pending amount lane",
|
|
"[region+0x25e] severity/source lane",
|
|
],
|
|
&["The queued kind-7 notice family is not obviously persisted in ordinary saves, so the pending queue must be treated as transient until a direct owner seam is found."],
|
|
),
|
|
build_service_trace_branch_status(
|
|
"city_connection_completion",
|
|
"blocked_missing_completion_and_one_shot_latches",
|
|
&[
|
|
"region triplet envelope",
|
|
"region name stem",
|
|
],
|
|
&[
|
|
"[region+0x302] completion latch",
|
|
"[region+0x316] one-shot notice latch",
|
|
"stable region id or class discriminator",
|
|
],
|
|
&["The remaining region blocker is a separate owner seam for the latches the city-connection branch reads and writes."],
|
|
),
|
|
],
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let mut notes = Vec::new();
|
|
notes.push(
|
|
"Region service trace treats the queued kind-7 notice family as transient runtime state until a persisted owner seam is found.".to_string(),
|
|
);
|
|
notes.push(
|
|
"The current region seam is strong enough to prove record-envelope ownership, profile subcollection ownership, and the absence of hidden 0x55f3 tail padding on grounded saves.".to_string(),
|
|
);
|
|
if let Some(probe) = analysis.region_record_triplets.as_ref() {
|
|
let mut trailing_words = probe
|
|
.entries
|
|
.iter()
|
|
.map(|entry| entry.policy_trailing_word_hex.clone())
|
|
.collect::<Vec<_>>();
|
|
trailing_words.sort();
|
|
trailing_words.dedup();
|
|
let preview = trailing_words
|
|
.iter()
|
|
.take(4)
|
|
.cloned()
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
notes.push(format!(
|
|
"Region 0x55f2 trailing-word candidates currently collapse to {} distinct value(s): {}.",
|
|
trailing_words.len(),
|
|
if preview.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
preview
|
|
}
|
|
));
|
|
if probe.entries.iter().all(|entry| {
|
|
entry.policy_reserved_dwords.iter().all(|word| *word == 0)
|
|
&& entry.policy_trailing_word == 1
|
|
}) {
|
|
notes.push(
|
|
"Grounded region 0x55f2 fixed-policy chunks also keep all three reserved dwords at zero while the trailing word stays 0x0001, so that chunk is not currently carrying the missing latch/id discriminator."
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
SmpRegionServiceTraceReport {
|
|
profile_family: analysis.profile_family.clone(),
|
|
region_collection_header_present: analysis.region_collection_header.is_some(),
|
|
region_record_triplet_count: analysis
|
|
.region_record_triplets
|
|
.as_ref()
|
|
.map(|probe| probe.record_count)
|
|
.unwrap_or_default(),
|
|
queued_notice_record_count: analysis
|
|
.region_queued_notice_records
|
|
.as_ref()
|
|
.map(|probe| probe.entries.len())
|
|
.unwrap_or_default(),
|
|
atlas_candidate_consumers,
|
|
known_owner_bridge_fields,
|
|
known_bridge_helpers,
|
|
next_owner_questions,
|
|
candidate_consumer_hypotheses,
|
|
entries,
|
|
notes,
|
|
}
|
|
}
|
|
|
|
fn build_infrastructure_asset_trace_report(
|
|
analysis: &SmpSaveCompanyChairmanAnalysisReport,
|
|
) -> SmpInfrastructureAssetTraceReport {
|
|
let side_buffer = analysis.placed_structure_dynamic_side_buffer.as_ref();
|
|
let alignment = analysis
|
|
.placed_structure_dynamic_side_buffer_alignment
|
|
.as_ref();
|
|
let name_pair_summaries = side_buffer
|
|
.map(|probe| probe.name_pair_summaries.as_slice())
|
|
.unwrap_or(&[]);
|
|
let bridge_like_name_pair_count = name_pair_summaries
|
|
.iter()
|
|
.filter(|summary| summary.primary_name.contains("Bridge"))
|
|
.count();
|
|
let tunnel_like_name_pair_count = name_pair_summaries
|
|
.iter()
|
|
.filter(|summary| summary.primary_name.contains("Tunnel"))
|
|
.count();
|
|
let track_cap_like_name_pair_count = name_pair_summaries
|
|
.iter()
|
|
.filter(|summary| summary.primary_name.contains("TrackCap"))
|
|
.count();
|
|
let atlas_candidate_consumers = vec![
|
|
"0x00493be0 infrastructure tagged side-buffer collection load owner".to_string(),
|
|
"0x0048a1e0 infrastructure child attach helper".to_string(),
|
|
"0x0048dcf0 infrastructure tagged child-stream restore outer owner".to_string(),
|
|
"0x0048dd50 infrastructure child rebuild loop".to_string(),
|
|
"0x00490a3c infrastructure payload attach helper".to_string(),
|
|
"0x004559d0 infrastructure tagged string-triplet serializer".to_string(),
|
|
"0x00455870 infrastructure tagged string-triplet load companion".to_string(),
|
|
"0x00455930 infrastructure scalar-triplet serializer sibling".to_string(),
|
|
"0x00448a70 / 0x00493660 / 0x0048b660 route and world follow-on family".to_string(),
|
|
"0x004133b0 placed-structure local-runtime refresh outer owner".to_string(),
|
|
];
|
|
let known_owner_bridge_fields = vec![
|
|
"[this+0x248] cached primary-child slot".to_string(),
|
|
"[this+0x206/+0x20a/+0x20e] route-entry resolver fields".to_string(),
|
|
"[this+0x1e2/+0x1e6/+0x1ea] published anchor triplet".to_string(),
|
|
"child list [this+0x75] under the Infrastructure owner".to_string(),
|
|
"non-direct live-entry directory [collection+0x3c] with 12-byte rows (payload pointer, previous live id, next live id)".to_string(),
|
|
];
|
|
let known_bridge_helpers = vec![
|
|
"0x00493be0 tagged 0x38a5/0x38a6/0x38a7 collection load owner".to_string(),
|
|
"0x00455a50 raw vtable slot +0x40 dispatch wrapper with global bridge reset".to_string(),
|
|
"0x00518140 indexed_collection_resolve_live_entry_payload_pointer_by_live_id".to_string(),
|
|
"0x005181f0 indexed_collection_unlink_non_direct_live_entry".to_string(),
|
|
"0x00518260 indexed_collection_link_non_direct_live_entry".to_string(),
|
|
"0x00518380 indexed_collection_find_nth_live_entry_id".to_string(),
|
|
"0x00518680 indexed_collection_load_header_bitset_and_non_direct_tables".to_string(),
|
|
"0x005395d0 shared child-attach list owner".to_string(),
|
|
"0x00539530 shared position-lane seed helper".to_string(),
|
|
"0x0053a5b0 shared third position-lane seed helper".to_string(),
|
|
"0x00530720 runtime_object_publish_anchor_triplet_and_optionally_rebind_world_cell_handle"
|
|
.to_string(),
|
|
"0x0048e140 / 0x0048e160 / 0x0048e180 route-entry resolver helpers".to_string(),
|
|
];
|
|
let next_owner_questions = vec![
|
|
"Which exact 0x38a5 rows or compact-prefix regimes feed the child count and optional primary-child ordinal that 0x0048dcf0 restores before the later rebuild loop runs?".to_string(),
|
|
"Which 0x38a5 embedded name-pair groups survive into the per-child vtable +0x40 payload callbacks dispatched through 0x00455a50?".to_string(),
|
|
"Is cached primary-child slot [this+0x248] the first owner-visible bridge from the restored child stream into route-entry rebuild?".to_string(),
|
|
];
|
|
let candidate_consumer_hypotheses = vec![
|
|
SmpServiceConsumerHypothesis {
|
|
label: "infrastructure child attach/rebuild path".to_string(),
|
|
status: if side_buffer.is_some() && (bridge_like_name_pair_count > 0
|
|
|| tunnel_like_name_pair_count > 0
|
|
|| track_cap_like_name_pair_count > 0)
|
|
{
|
|
"highest_priority_static_mapping_target".to_string()
|
|
} else {
|
|
"possible_consumer_family".to_string()
|
|
},
|
|
candidate_consumers: vec![
|
|
"0x00493be0 infrastructure tagged side-buffer collection load owner".to_string(),
|
|
"0x0048a1e0 infrastructure child attach helper".to_string(),
|
|
"0x0048dcf0 infrastructure tagged child-stream restore outer owner".to_string(),
|
|
"0x0048dd50 infrastructure child rebuild loop".to_string(),
|
|
"0x00490a3c infrastructure payload attach helper".to_string(),
|
|
],
|
|
evidence: vec![
|
|
format!(
|
|
"real side-buffer name families currently count bridge/tunnel/track-cap pairs as {}/{}/{}",
|
|
bridge_like_name_pair_count,
|
|
tunnel_like_name_pair_count,
|
|
track_cap_like_name_pair_count
|
|
),
|
|
"atlas already bounds these helpers under the literal Infrastructure owner".to_string(),
|
|
"the side-buffer corpus is disjoint from the placed-structure triplet corpus, so a separate child/rebuild family is more plausible than a compact alias".to_string(),
|
|
"direct disassembly now shows 0x00493be0 opening tag family 0x38a5/0x38a6/0x38a7, reading one shared dword into the owner-local 0x90/0x94 lane, iterating each live collection entry, and dispatching every loaded infrastructure record through 0x0048dcf0 before the later follow-on owners run".to_string(),
|
|
"direct disassembly now shows 0x00518140 resolving a non-direct live entry through the tombstone bitset and then returning the first dword of a 12-byte row from [collection+0x3c] for the 0x38a5 path".to_string(),
|
|
"direct disassembly now also shows 0x005181f0/0x00518260 treating the same 12-byte rows as a live-entry directory: dword +0 is the payload pointer, dword +4 is previous live id, and dword +8 is next live id, with collection head/tail caches alongside them".to_string(),
|
|
"direct disassembly now also shows 0x00493be0 iterating live-entry ordinals through 0x00518380(ordinal, 0), converting each ordinal to a live id, then resolving that live id through 0x00518140 before handing the resulting payload pointer to 0x0048dcf0".to_string(),
|
|
"direct disassembly now shows 0x00518680 loading the non-direct collection header, tombstone bitset, and live-id-bound-scaled 12-byte tables for the non-direct path before 0x00493be0 starts iterating".to_string(),
|
|
"direct disassembly now also shows the shared child payload callback 0x00455fc0 opening 0x55f1, parsing three len-prefixed strings through 0x531380, opening 0x55f2, seeding the child through 0x455b70, dispatching slot +0x48, and then opening 0x55f3".to_string(),
|
|
format!(
|
|
"current save-side probe reports {} embedded 0x55f1 rows with a third decoded string",
|
|
side_buffer
|
|
.map(|probe| probe.decoded_embedded_name_row_with_tertiary_name_count)
|
|
.unwrap_or_default()
|
|
),
|
|
"local .rdata at 0x005cfd00 now also proves the infrastructure child table uses the shared tagged callback strip directly: slot +0x40 = 0x455fc0, slot +0x48 = 0x455870, and slot +0x4c = 0x455930".to_string(),
|
|
"direct disassembly now shows 0x0048a1e0 cloning the first child triplet bands through 0x52e880/0x52e720, destroying the prior child, seeding a new literal Infrastructure child through 0x455b70 with payload seed 0x5c87a8, attaching through 0x5395d0 or 0x53a5d0, and republishing the two bands through 0x52e8b0/0x530720".to_string(),
|
|
"direct disassembly now also shows the outer owner at 0x0048dcf0 reading a child count plus optional primary-child ordinal from the tagged stream through 0x531150, zeroing [this+0x08], dispatching each fresh child through 0x455a50 -> vtable slot +0x40, culling ordinals above 5, and restoring cached primary-child slot [this+0x248] from the saved ordinal".to_string(),
|
|
"the smaller attach primitive 0x00490a3c no longer looks like the semantic fork by itself: it just allocates one literal Infrastructure child, seeds it through 0x455b70 with caller-provided stem input, attaches it through 0x5395d0, seeds position lanes through 0x539530/0x53a5b0, and optionally caches it as primary child".to_string(),
|
|
],
|
|
blockers: vec![
|
|
"how the payload streams reached through 0x00518380 -> 0x00518140 align with the embedded 0x55f1 name-pair groups and compact-prefix regimes surfaced by the save-side probe".to_string(),
|
|
"which tagged values inside each payload stream correspond to the child count, optional primary-child ordinal, and the per-child shared tagged callback sequence consumed by 0x0048dcf0".to_string(),
|
|
"whether the third 0x55f1 string parsed by 0x00455fc0 is absent on grounded saves, stored under a different framing than the current probe, or only populated on a narrower infrastructure subset".to_string(),
|
|
"which restored child fields or grouped rows retain the 0x38a5 embedded name-pair semantics before route/local-runtime follow-ons take over".to_string(),
|
|
],
|
|
},
|
|
SmpServiceConsumerHypothesis {
|
|
label: "infrastructure serializer/load companion path".to_string(),
|
|
status: if side_buffer.is_some() {
|
|
"strong_static_mapping_candidate".to_string()
|
|
} else {
|
|
"possible_consumer_family".to_string()
|
|
},
|
|
candidate_consumers: vec![
|
|
"0x004559d0 infrastructure tagged string-triplet serializer".to_string(),
|
|
"0x00455870 infrastructure tagged string-triplet load companion".to_string(),
|
|
"0x00455930 infrastructure scalar-triplet serializer sibling".to_string(),
|
|
],
|
|
evidence: vec![
|
|
"atlas already bounds the serializer/load strip around the Infrastructure owner and the same 0x55f1/0x55f2/0x55f3 tag family".to_string(),
|
|
"local .rdata at 0x005cfd00 now proves the infrastructure child vtable points straight at 0x455fc0/0x455870/0x455930 for the load, triplet-restore, and serializer slots".to_string(),
|
|
"the save-side side-buffer carries embedded dual-name rows plus compact prefixes, which is compatible with a serializer-side bridge".to_string(),
|
|
],
|
|
blockers: vec![
|
|
"which exact 0x38a5 rows belong to shared 0x55f1/0x55f2/0x55f3 child records versus outer collection metadata".to_string(),
|
|
],
|
|
},
|
|
SmpServiceConsumerHypothesis {
|
|
label: "route/local-runtime follow-on path".to_string(),
|
|
status: if side_buffer.is_some() {
|
|
"secondary_candidate_after_attach_rebuild".to_string()
|
|
} else {
|
|
"possible_consumer_family".to_string()
|
|
},
|
|
candidate_consumers: vec![
|
|
"0x00448a70 / 0x00493660 / 0x0048b660 route and world follow-on family".to_string(),
|
|
"0x004133b0 placed-structure local-runtime refresh outer owner".to_string(),
|
|
],
|
|
evidence: vec![
|
|
"atlas ties the Infrastructure rebuild loop to later route-side and local-runtime follow-on owners".to_string(),
|
|
"current side-buffer trace shows separate infrastructure state but does not yet prove direct route-entry consumption".to_string(),
|
|
],
|
|
blockers: vec![
|
|
"needs one upstream consumer or rebuild owner first".to_string(),
|
|
"no direct save-side evidence yet for route-entry or local-runtime field linkage".to_string(),
|
|
],
|
|
},
|
|
];
|
|
let branches = vec![
|
|
build_service_trace_branch_status(
|
|
"infrastructure_asset_owner_seam",
|
|
if side_buffer.is_some() {
|
|
"grounded_separate_owner_seam"
|
|
} else {
|
|
"blocked_missing_side_buffer_owner_seam"
|
|
},
|
|
&[
|
|
"0x38a5/0x38a6/0x38a7 tagged family",
|
|
"embedded 0x55f1 dual-name rows",
|
|
"compact 6-byte prefix regimes",
|
|
],
|
|
if side_buffer.is_some() {
|
|
&[]
|
|
} else {
|
|
&["0x38a5 owner seam"]
|
|
},
|
|
&[
|
|
"This seam should be treated as infrastructure-asset state rather than as a compact alias of placed-structure triplets.",
|
|
],
|
|
),
|
|
build_service_trace_branch_status(
|
|
"placed_structure_triplet_alias",
|
|
if alignment.is_some_and(|probe| probe.overlapping_name_pair_count == 0) {
|
|
"disproved_by_grounded_probe"
|
|
} else {
|
|
"unresolved"
|
|
},
|
|
&[
|
|
"0x36b1 placed-structure triplet corpus",
|
|
"0x38a5 side-buffer name-pair corpus",
|
|
],
|
|
&[],
|
|
&[
|
|
"Grounded q.gms evidence currently shows zero overlap between the side-buffer name-pair corpus and the placed-structure triplet name-pair corpus.",
|
|
],
|
|
),
|
|
build_service_trace_branch_status(
|
|
"city_connection_consumer_mapping",
|
|
"blocked_missing_infrastructure_asset_consumer_mapping",
|
|
&[
|
|
"grounded 0x38a5 owner seam",
|
|
"placed-structure triplet seam",
|
|
],
|
|
&[
|
|
"higher-layer consumer dispatch mapping",
|
|
"compact prefix regime semantics",
|
|
],
|
|
&[
|
|
"The remaining problem is how higher-layer service code consumes this separate seam, not whether the seam exists.",
|
|
],
|
|
),
|
|
build_service_trace_branch_status(
|
|
"linked_transit_consumer_mapping",
|
|
"blocked_missing_infrastructure_asset_consumer_mapping",
|
|
&["grounded 0x38a5 owner seam", "company linked-transit latch"],
|
|
&[
|
|
"side-buffer consumer mapping",
|
|
"route or roster rebuild owner path",
|
|
],
|
|
&[
|
|
"The next slice should target the consumer path above the side-buffer seam rather than another raw save scan.",
|
|
],
|
|
),
|
|
];
|
|
let notes = vec![
|
|
"Infrastructure asset trace now makes the side-buffer-versus-triplet split explicit: owner seam identity is grounded, consumer semantics are still blocked.".to_string(),
|
|
];
|
|
SmpInfrastructureAssetTraceReport {
|
|
profile_family: analysis.profile_family.clone(),
|
|
placed_structure_collection_header_present: analysis
|
|
.placed_structure_collection_header
|
|
.is_some(),
|
|
placed_structure_record_triplet_count: analysis
|
|
.placed_structure_record_triplets
|
|
.as_ref()
|
|
.map(|probe| probe.record_count)
|
|
.unwrap_or_default(),
|
|
side_buffer_present: side_buffer.is_some(),
|
|
side_buffer_decoded_embedded_name_row_count: side_buffer
|
|
.map(|probe| probe.decoded_embedded_name_row_count)
|
|
.unwrap_or_default(),
|
|
side_buffer_unique_name_pair_count: side_buffer
|
|
.map(|probe| probe.unique_embedded_name_pair_count)
|
|
.unwrap_or_default(),
|
|
bridge_like_name_pair_count,
|
|
tunnel_like_name_pair_count,
|
|
track_cap_like_name_pair_count,
|
|
triplet_alignment_overlap_count: alignment
|
|
.map(|probe| probe.overlapping_name_pair_count)
|
|
.unwrap_or_default(),
|
|
atlas_candidate_consumers,
|
|
known_owner_bridge_fields,
|
|
known_bridge_helpers,
|
|
next_owner_questions,
|
|
candidate_consumer_hypotheses,
|
|
branches,
|
|
notes,
|
|
}
|
|
}
|
|
|
|
pub fn inspect_smp_bytes(bytes: &[u8]) -> SmpInspectionReport {
|
|
inspect_bundle_bytes(bytes, None)
|
|
}
|
|
|
|
pub fn load_save_slice_file(path: &Path) -> Result<SmpLoadedSaveSlice, Box<dyn std::error::Error>> {
|
|
let inspection = inspect_smp_file(path)?;
|
|
load_save_slice_from_report(&inspection)
|
|
.map_err(|err| -> Box<dyn std::error::Error> { err.into() })
|
|
}
|
|
|
|
pub fn load_save_slice_from_report(
|
|
report: &SmpInspectionReport,
|
|
) -> Result<SmpLoadedSaveSlice, String> {
|
|
let summary = report
|
|
.save_load_summary
|
|
.as_ref()
|
|
.ok_or_else(|| "inspection did not expose a recognizable save-load summary".to_string())?;
|
|
let profile = if let Some(probe) = &report.classic_rehydrate_profile_probe {
|
|
Some(SmpLoadedProfile {
|
|
profile_kind: "classic-rehydrate-profile".to_string(),
|
|
profile_family: probe.profile_family.clone(),
|
|
packed_profile_offset: probe.packed_profile_offset,
|
|
packed_profile_len: probe.packed_profile_len,
|
|
packed_profile_len_hex: probe.packed_profile_len_hex.clone(),
|
|
leading_word_0: probe.packed_profile_block.leading_word_0,
|
|
leading_word_0_hex: probe.packed_profile_block.leading_word_0_hex.clone(),
|
|
header_flag_word_3: None,
|
|
header_flag_word_3_hex: None,
|
|
map_path: probe.packed_profile_block.map_path.clone(),
|
|
display_name: probe.packed_profile_block.display_name.clone(),
|
|
profile_byte_0x77: probe.packed_profile_block.profile_byte_0x77,
|
|
profile_byte_0x77_hex: probe.packed_profile_block.profile_byte_0x77_hex.clone(),
|
|
profile_byte_0x82: probe.packed_profile_block.profile_byte_0x82,
|
|
profile_byte_0x82_hex: probe.packed_profile_block.profile_byte_0x82_hex.clone(),
|
|
profile_byte_0x97: probe.packed_profile_block.profile_byte_0x97,
|
|
profile_byte_0x97_hex: probe.packed_profile_block.profile_byte_0x97_hex.clone(),
|
|
profile_byte_0xc5: probe.packed_profile_block.profile_byte_0xc5,
|
|
profile_byte_0xc5_hex: probe.packed_profile_block.profile_byte_0xc5_hex.clone(),
|
|
})
|
|
} else {
|
|
report
|
|
.rt3_105_packed_profile_probe
|
|
.as_ref()
|
|
.map(|probe| SmpLoadedProfile {
|
|
profile_kind: "rt3-105-packed-profile".to_string(),
|
|
profile_family: probe.profile_family.clone(),
|
|
packed_profile_offset: probe.packed_profile_offset,
|
|
packed_profile_len: probe.packed_profile_len,
|
|
packed_profile_len_hex: probe.packed_profile_len_hex.clone(),
|
|
leading_word_0: probe.packed_profile_block.leading_word_0,
|
|
leading_word_0_hex: probe.packed_profile_block.leading_word_0_hex.clone(),
|
|
header_flag_word_3: Some(probe.packed_profile_block.header_flag_word_3),
|
|
header_flag_word_3_hex: Some(
|
|
probe.packed_profile_block.header_flag_word_3_hex.clone(),
|
|
),
|
|
map_path: probe.packed_profile_block.map_path.clone(),
|
|
display_name: probe.packed_profile_block.display_name.clone(),
|
|
profile_byte_0x77: probe.packed_profile_block.profile_byte_0x77,
|
|
profile_byte_0x77_hex: probe.packed_profile_block.profile_byte_0x77_hex.clone(),
|
|
profile_byte_0x82: probe.packed_profile_block.profile_byte_0x82,
|
|
profile_byte_0x82_hex: probe.packed_profile_block.profile_byte_0x82_hex.clone(),
|
|
profile_byte_0x97: probe.packed_profile_block.profile_byte_0x97,
|
|
profile_byte_0x97_hex: probe.packed_profile_block.profile_byte_0x97_hex.clone(),
|
|
profile_byte_0xc5: probe.packed_profile_block.profile_byte_0xc5,
|
|
profile_byte_0xc5_hex: probe.packed_profile_block.profile_byte_0xc5_hex.clone(),
|
|
})
|
|
};
|
|
let candidate_availability_table = report.rt3_105_save_name_table_probe.as_ref().map(|probe| {
|
|
SmpLoadedCandidateAvailabilityTable {
|
|
source_kind: probe.source_kind.clone(),
|
|
semantic_family: probe.semantic_family.clone(),
|
|
header_offset: probe.header_offset,
|
|
entries_offset: probe.entries_offset,
|
|
entries_end_offset: probe.entries_end_offset,
|
|
observed_entry_count: probe.observed_entry_count,
|
|
zero_availability_count: probe.zero_trailer_entry_count,
|
|
zero_availability_names: probe.zero_trailer_entry_names.clone(),
|
|
footer_progress_hex_words: vec![
|
|
probe.footer_progress_word_0_hex.clone(),
|
|
probe.footer_progress_word_1_hex.clone(),
|
|
],
|
|
entries: probe.entries.clone(),
|
|
}
|
|
});
|
|
let named_locomotive_availability_table = report
|
|
.rt3_105_save_named_locomotive_availability_probe
|
|
.as_ref()
|
|
.map(|probe| SmpLoadedNamedLocomotiveAvailabilityTable {
|
|
source_kind: probe.source_kind.clone(),
|
|
semantic_family: probe.semantic_family.clone(),
|
|
header_offset: None,
|
|
entries_offset: Some(probe.entries_offset),
|
|
entries_end_offset: Some(probe.entries_end_offset),
|
|
observed_entry_count: probe.observed_entry_count,
|
|
zero_availability_count: probe.zero_availability_count,
|
|
zero_availability_names: probe.zero_availability_names.clone(),
|
|
entries: probe.entries.clone(),
|
|
});
|
|
let locomotive_catalog = named_locomotive_availability_table
|
|
.as_ref()
|
|
.and_then(derive_locomotive_catalog_from_named_availability_table);
|
|
let cargo_catalog = report
|
|
.recipe_book_summary_probe
|
|
.as_ref()
|
|
.and_then(derive_cargo_catalog_from_recipe_book_probe);
|
|
let world_issue_37_state = report
|
|
.save_world_issue_37_probe
|
|
.as_ref()
|
|
.map(derive_loaded_world_issue_37_state_from_probe);
|
|
let world_economic_tuning_state = report
|
|
.save_world_economic_tuning_probe
|
|
.as_ref()
|
|
.map(derive_loaded_world_economic_tuning_state_from_probe);
|
|
let world_finance_neighborhood_state = report
|
|
.save_world_finance_neighborhood_probe
|
|
.as_ref()
|
|
.map(derive_loaded_world_finance_neighborhood_state_from_probe);
|
|
let world_locomotive_policy_state = derive_loaded_world_locomotive_policy_state_from_probes(
|
|
report.post_text_field_neighborhood_probe.as_ref(),
|
|
report.locomotive_policy_neighborhood_probe.as_ref(),
|
|
);
|
|
let company_roster = report.save_company_roster_probe.clone().or_else(|| {
|
|
report
|
|
.save_world_selection_context_probe
|
|
.as_ref()
|
|
.and_then(|probe| {
|
|
derive_selection_only_company_roster_from_save_world_probe(
|
|
probe,
|
|
report.save_company_collection_header_probe.as_ref(),
|
|
)
|
|
})
|
|
});
|
|
let chairman_profile_table = report
|
|
.save_chairman_profile_table_probe
|
|
.clone()
|
|
.or_else(|| {
|
|
report
|
|
.save_world_selection_context_probe
|
|
.as_ref()
|
|
.and_then(|probe| {
|
|
derive_selection_only_chairman_profile_table_from_save_world_probe(
|
|
probe,
|
|
report
|
|
.save_chairman_profile_collection_header_probe
|
|
.as_ref(),
|
|
)
|
|
})
|
|
});
|
|
let special_conditions_table =
|
|
report
|
|
.special_conditions_probe
|
|
.as_ref()
|
|
.map(|probe| SmpLoadedSpecialConditionsTable {
|
|
source_kind: probe.source_kind.clone(),
|
|
table_offset: probe.table_offset,
|
|
table_len: probe.table_len,
|
|
enabled_visible_count: probe.enabled_visible_count,
|
|
enabled_visible_labels: probe.enabled_visible_labels.clone(),
|
|
entries: probe.entries.clone(),
|
|
});
|
|
let placed_structure_dynamic_side_buffer_probe = report
|
|
.save_placed_structure_dynamic_side_buffer_probe
|
|
.clone();
|
|
let mut notes = summary.notes.clone();
|
|
if let Some(probe) = &report.save_world_selection_context_probe {
|
|
notes.push(format!(
|
|
"Raw save fixed world block exposes selected_company_id={} at file offset 0x{:x}.",
|
|
probe.selected_company_id, probe.selected_company_id_offset
|
|
));
|
|
notes.push(format!(
|
|
"Raw save fixed world block exposes selected_chairman_profile_id={} at file offset 0x{:x}.",
|
|
probe.selected_chairman_profile_id, probe.selected_chairman_profile_id_offset
|
|
));
|
|
notes.push(format!(
|
|
"Raw save fixed world block also exposes {} chairman slot selector bytes at file offset 0x{:x} and campaign_override_flag={} at file offset 0x{:x}.",
|
|
probe.chairman_slot_selectors.len(),
|
|
probe.chairman_slot_selector_offset,
|
|
probe.campaign_override_flag,
|
|
probe.campaign_override_flag_offset
|
|
));
|
|
if report.save_company_roster_probe.is_none()
|
|
|| report.save_chairman_profile_table_probe.is_none()
|
|
{
|
|
notes.push(
|
|
"Raw save inspection still does not reconstruct every company_roster or chairman_profile_table scalar lane; the grounded package-save path prefers direct-record reconstruction where it can and falls back to selection/header-only context otherwise."
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
if let Some(probe) = &report.save_world_issue_37_probe {
|
|
notes.push(format!(
|
|
"Raw save fixed world block also exposes the grounded issue-0x37 pair: value={} at file offset 0x{:x} and companion multiplier {:.6} at file offset 0x{:x}.",
|
|
probe.issue_value_lane.value_i32,
|
|
probe.payload_offset + probe.issue_value_lane.relative_offset,
|
|
probe.multiplier_lane.value_f32,
|
|
probe.payload_offset + probe.multiplier_lane.relative_offset
|
|
));
|
|
}
|
|
if let Some(probe) = &report.save_world_economic_tuning_probe {
|
|
notes.push(format!(
|
|
"Raw save fixed world block also exposes the six-lane economic tuning float band at file offset 0x{:x} (mirror lane at 0x{:x}).",
|
|
probe.tuning_lanes
|
|
.first()
|
|
.map(|lane| probe.payload_offset + lane.relative_offset)
|
|
.unwrap_or(probe.payload_offset),
|
|
probe.payload_offset + probe.mirror_lane.relative_offset
|
|
));
|
|
notes.push(
|
|
"Current atlas evidence treats that fixed six-float world tuning band as the editor economic-cost family, not as the company-governance issue table behind investor confidence."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if let Some(probe) = &report.save_company_collection_header_probe {
|
|
notes.push(format!(
|
|
"Raw save tagged company header reports live_record_count={} and live_id_bound={} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
|
|
probe.live_record_count,
|
|
probe.live_id_bound,
|
|
probe.metadata_tag_offset,
|
|
probe.records_tag_offset,
|
|
probe.close_tag_offset
|
|
));
|
|
}
|
|
if let Some(probe) = &report.save_chairman_profile_collection_header_probe {
|
|
notes.push(format!(
|
|
"Raw save tagged chairman/profile header reports live_record_count={} and live_id_bound={} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
|
|
probe.live_record_count,
|
|
probe.live_id_bound,
|
|
probe.metadata_tag_offset,
|
|
probe.records_tag_offset,
|
|
probe.close_tag_offset
|
|
));
|
|
}
|
|
if let Some(probe) = &report.save_train_collection_header_probe {
|
|
notes.push(format!(
|
|
"Raw save tagged train header reports live_record_count={} and live_id_bound={} with direct_record_stride=0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
|
|
probe.live_record_count,
|
|
probe.live_id_bound,
|
|
probe.direct_record_stride,
|
|
probe.metadata_tag_offset,
|
|
probe.records_tag_offset,
|
|
probe.close_tag_offset
|
|
));
|
|
}
|
|
if let Some(probe) = &report.save_train_collection_directory_probe {
|
|
notes.push(format!(
|
|
"Raw save tagged train metadata also exposes a live-entry directory with {} entries rooted at metadata dword {} (head={:?}, tail={:?}).",
|
|
probe.entries.len(),
|
|
probe.directory_root_dword_index,
|
|
probe.chain_head_live_entry_id,
|
|
probe.chain_tail_live_entry_id
|
|
));
|
|
}
|
|
if let Some(probe) = &report.save_region_collection_header_probe {
|
|
notes.push(format!(
|
|
"Raw save tagged region header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
|
|
probe.live_record_count,
|
|
probe.live_id_bound,
|
|
probe.direct_record_stride,
|
|
probe.metadata_tag_offset,
|
|
probe.records_tag_offset,
|
|
probe.close_tag_offset
|
|
));
|
|
}
|
|
if let Some(probe) = &report.save_region_record_triplet_probe {
|
|
notes.push(format!(
|
|
"Raw save tagged region records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets in the records span; first name={:?}, first policy lanes=({:.3}, {:.3}, {:.3}), trailing_word={}, first profile collection count={:?}, first profile collection trailing_padding_len={:?}.",
|
|
probe.record_count,
|
|
probe.entries.first().map(|entry| entry.name.as_str()),
|
|
probe.entries
|
|
.first()
|
|
.map(|entry| entry.policy_leading_f32_0)
|
|
.unwrap_or_default(),
|
|
probe.entries
|
|
.first()
|
|
.map(|entry| entry.policy_leading_f32_1)
|
|
.unwrap_or_default(),
|
|
probe.entries
|
|
.first()
|
|
.map(|entry| entry.policy_leading_f32_2)
|
|
.unwrap_or_default(),
|
|
probe.entries
|
|
.first()
|
|
.map(|entry| entry.policy_trailing_word_hex.as_str())
|
|
.unwrap_or("0x0000"),
|
|
probe.entries.first().and_then(|entry| {
|
|
entry.profile_collection.as_ref().map(|collection| collection.live_record_count)
|
|
}),
|
|
probe.entries.first().and_then(|entry| {
|
|
entry.profile_collection.as_ref().map(|collection| collection.trailing_padding_len)
|
|
})
|
|
));
|
|
}
|
|
if let Some(probe) = &report.save_placed_structure_collection_header_probe {
|
|
notes.push(format!(
|
|
"Raw save tagged placed-structure header reports live_record_count={} and live_id_bound={} with serialized stride hint 0x{:x} at file offsets 0x{:x}/0x{:x}/0x{:x}.",
|
|
probe.live_record_count,
|
|
probe.live_id_bound,
|
|
probe.direct_record_stride,
|
|
probe.metadata_tag_offset,
|
|
probe.records_tag_offset,
|
|
probe.close_tag_offset
|
|
));
|
|
}
|
|
if let Some(probe) = &report.save_placed_structure_record_triplet_probe {
|
|
notes.push(format!(
|
|
"Raw save tagged placed-structure records also expose {} repeated 0x55f1/0x55f2/0x55f3 triplets; first stems={:?}/{:?}, first policy lanes=({:.3}, {:.3}, {:.3}, {:.3}, {:.3}), first footer payload={}, first footer status kind={:?}.",
|
|
probe.record_count,
|
|
probe.entries.first().map(|entry| entry.primary_name.as_str()),
|
|
probe.entries.first().map(|entry| entry.secondary_name.as_str()),
|
|
probe.entries.first().map(|entry| entry.policy_f32_lane_0).unwrap_or_default(),
|
|
probe.entries.first().map(|entry| entry.policy_f32_lane_1).unwrap_or_default(),
|
|
probe.entries.first().map(|entry| entry.policy_f32_lane_2).unwrap_or_default(),
|
|
probe.entries.first().map(|entry| entry.policy_f32_lane_3).unwrap_or_default(),
|
|
probe.entries.first().map(|entry| entry.policy_f32_lane_4).unwrap_or_default(),
|
|
probe.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000"),
|
|
probe.entries.first().map(|entry| entry.profile_status_kind.as_str())
|
|
));
|
|
}
|
|
if let Some(probe) = &placed_structure_dynamic_side_buffer_probe {
|
|
let dominant_pattern = probe.compact_prefix_pattern_summaries.first();
|
|
notes.push(format!(
|
|
"Raw save also exposes the separate placed-structure dynamic-side-buffer candidate 0x38a5/0x38a6/0x38a7: live_record_count={}, owner-shared 0x38a6 dword={} at relative offset 0x{:x}, first compact prefix=({},{},{}), first embedded names={:?}/{:?}/{:?}, embedded 0x55f1 row count={}, rows with tertiary 0x55f1 string={}, unique compact prefix patterns={}, 0x55f3-leading rows={}, dominant compact pattern={}/{}/{} x{}.",
|
|
probe.live_record_count,
|
|
probe.owner_shared_dword_hex,
|
|
probe.owner_shared_dword_relative_offset,
|
|
probe.prefix_leading_dword_hex,
|
|
probe.prefix_trailing_word_hex,
|
|
probe.prefix_separator_byte_hex,
|
|
probe.first_embedded_primary_name.as_deref(),
|
|
probe.first_embedded_secondary_name.as_deref(),
|
|
probe.first_embedded_tertiary_name.as_deref(),
|
|
probe.embedded_name_tag_count,
|
|
probe.decoded_embedded_name_row_with_tertiary_name_count,
|
|
probe.unique_compact_prefix_pattern_count,
|
|
probe.prefix_leading_dword_matching_embedded_profile_tag_count,
|
|
dominant_pattern
|
|
.map(|pattern| pattern.prefix_leading_dword_hex.as_str())
|
|
.unwrap_or("0x00000000"),
|
|
dominant_pattern
|
|
.map(|pattern| pattern.prefix_trailing_word_hex.as_str())
|
|
.unwrap_or("0x0000"),
|
|
dominant_pattern
|
|
.map(|pattern| pattern.prefix_separator_byte_hex.as_str())
|
|
.unwrap_or("0x00"),
|
|
dominant_pattern.map(|pattern| pattern.count).unwrap_or_default()
|
|
));
|
|
if probe.owner_shared_dword_matches_first_compact_prefix_leading_dword {
|
|
notes.push(
|
|
"Direct disassembly now shows 0x00493be0 consuming one shared 0x38a6 owner-local dword before iterating records; the first compact-prefix leading dword currently reuses that same lane."
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
if let Some(roster) = &report.save_company_roster_probe {
|
|
notes.push(format!(
|
|
"Raw save inspection reconstructed {} company direct records from the tagged company collection.",
|
|
roster.entries.len()
|
|
));
|
|
}
|
|
if let Some(table) = &report.save_chairman_profile_table_probe {
|
|
notes.push(format!(
|
|
"Raw save inspection reconstructed {} chairman/profile direct records from the tagged chairman collection.",
|
|
table.entries.len()
|
|
));
|
|
}
|
|
|
|
Ok(SmpLoadedSaveSlice {
|
|
file_extension_hint: summary.file_extension_hint.clone(),
|
|
container_profile_family: summary.container_profile_family.clone(),
|
|
mechanism_family: summary.mechanism_family.clone(),
|
|
mechanism_confidence: summary.mechanism_confidence.clone(),
|
|
trailer_family: summary.trailer_family.clone(),
|
|
bridge_family: summary.bridge_family.clone(),
|
|
profile,
|
|
candidate_availability_table,
|
|
named_locomotive_availability_table,
|
|
locomotive_catalog,
|
|
cargo_catalog,
|
|
world_issue_37_state,
|
|
world_economic_tuning_state,
|
|
world_finance_neighborhood_state,
|
|
world_locomotive_policy_state,
|
|
company_roster,
|
|
chairman_profile_table,
|
|
special_conditions_table,
|
|
event_runtime_collection: report.event_runtime_collection_summary.clone(),
|
|
notes,
|
|
})
|
|
}
|
|
|
|
pub fn inspect_save_company_and_chairman_analysis_file(
|
|
path: &Path,
|
|
) -> Result<SmpSaveCompanyChairmanAnalysisReport, Box<dyn std::error::Error>> {
|
|
let bytes = fs::read(path)?;
|
|
let report = inspect_bundle_bytes(
|
|
&bytes,
|
|
path.extension()
|
|
.and_then(|extension| extension.to_str())
|
|
.map(|extension| extension.to_ascii_lowercase()),
|
|
);
|
|
inspect_save_company_and_chairman_analysis_bytes(&bytes, &report)
|
|
.ok_or_else(|| "save inspection did not expose grounded company/chairman analysis".into())
|
|
}
|
|
|
|
pub fn inspect_save_company_and_chairman_analysis_bytes(
|
|
bytes: &[u8],
|
|
report: &SmpInspectionReport,
|
|
) -> Option<SmpSaveCompanyChairmanAnalysisReport> {
|
|
let selection_probe = report.save_world_selection_context_probe.as_ref();
|
|
let world_selection_context = selection_probe.map(build_save_world_selection_role_analysis);
|
|
let world_issue_37 = report.save_world_issue_37_probe.clone();
|
|
let world_economic_tuning = report.save_world_economic_tuning_probe.clone();
|
|
let world_finance_neighborhood = report.save_world_finance_neighborhood_probe.clone();
|
|
let train_collection_directory = report.save_train_collection_directory_probe.clone();
|
|
let region_record_triplets = report.save_region_record_triplet_probe.clone();
|
|
let region_queued_notice_records = report
|
|
.save_region_queued_notice_record_probe
|
|
.clone()
|
|
.or_else(|| {
|
|
parse_save_region_queued_notice_record_probe(
|
|
bytes,
|
|
report.file_extension_hint.as_deref(),
|
|
report.container_profile.as_ref(),
|
|
report.save_region_collection_header_probe.as_ref(),
|
|
)
|
|
});
|
|
let placed_structure_record_triplets =
|
|
report.save_placed_structure_record_triplet_probe.clone();
|
|
let placed_structure_dynamic_side_buffer = report
|
|
.save_placed_structure_dynamic_side_buffer_probe
|
|
.clone()
|
|
.or_else(|| {
|
|
parse_save_placed_structure_dynamic_side_buffer_probe(
|
|
bytes,
|
|
report.file_extension_hint.as_deref(),
|
|
report.container_profile.as_ref(),
|
|
)
|
|
});
|
|
let placed_structure_dynamic_side_buffer_alignment = placed_structure_dynamic_side_buffer
|
|
.as_ref()
|
|
.zip(placed_structure_record_triplets.as_ref())
|
|
.map(|(side_buffer, triplets)| {
|
|
summarize_placed_structure_dynamic_side_buffer_alignment(side_buffer, triplets)
|
|
});
|
|
let unclassified_tagged_collection_headers = report
|
|
.save_unclassified_tagged_collection_header_probes
|
|
.clone();
|
|
let company_header_probe = report.save_company_collection_header_probe.as_ref();
|
|
let chairman_header_probe = report
|
|
.save_chairman_profile_collection_header_probe
|
|
.as_ref();
|
|
|
|
let company_entries = if let Some(header_probe) = company_header_probe {
|
|
let record_start_offset = detect_save_company_record_start_offset(&bytes, header_probe)?;
|
|
let record_stride = header_probe.direct_record_stride as usize;
|
|
let base_offset = header_probe
|
|
.metadata_tag_offset
|
|
.checked_add(4)?
|
|
.checked_add(record_start_offset)?;
|
|
let mut entries = Vec::with_capacity(header_probe.live_record_count as usize);
|
|
for index in 0..header_probe.live_record_count as usize {
|
|
let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?;
|
|
let company_id = read_u32_at(&bytes, record_offset)?;
|
|
let name = read_ascii_c_string_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_NAME_OFFSET,
|
|
SAVE_COMPANY_RECORD_NAME_MAX_LEN,
|
|
)?;
|
|
let active =
|
|
read_u8_at(&bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET)? != 0;
|
|
let linked_chairman_profile_id = parse_nonzero_u32(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET,
|
|
)?;
|
|
let outstanding_shares = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET,
|
|
)?;
|
|
let debt = parse_save_company_total_debt(&bytes, record_offset)?;
|
|
let bond_count = read_u8_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET,
|
|
)?;
|
|
let live_bond_slots = parse_save_company_live_bond_slots(&bytes, record_offset)?;
|
|
let largest_live_bond_principal =
|
|
parse_save_company_largest_live_bond_principal(&bytes, record_offset)?;
|
|
let highest_coupon_live_bond_principal =
|
|
parse_save_company_highest_coupon_live_bond_principal(&bytes, record_offset)?;
|
|
let available_track_laying_capacity =
|
|
parse_save_company_available_track_laying_capacity(&bytes, record_offset)?;
|
|
let company_value_scalar_f32 = read_f32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET,
|
|
)?;
|
|
let cached_share_support_scalar_f32 = read_f32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET,
|
|
)?;
|
|
let cached_share_price_f32 = read_f32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET,
|
|
)?;
|
|
let chairman_salary_baseline = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET,
|
|
)?;
|
|
let chairman_salary_current = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET,
|
|
)?;
|
|
let chairman_bonus_year = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET,
|
|
)?;
|
|
let chairman_bonus_amount = read_i32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET,
|
|
)?;
|
|
let founding_year = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET,
|
|
)?;
|
|
let last_bankruptcy_year = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET,
|
|
)?;
|
|
let last_dividend_year = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET,
|
|
)?;
|
|
let preferred_locomotive_engine_type_raw_u8 = read_u8_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET,
|
|
)?;
|
|
let city_connection_latch = read_u8_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET,
|
|
)? != 0;
|
|
let linked_transit_latch = read_u8_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET,
|
|
)? != 0;
|
|
let merger_cooldown_year = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET,
|
|
)?;
|
|
let takeover_cooldown_year = read_u32_at(
|
|
&bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET,
|
|
)?;
|
|
let scalar_dword_candidates = SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS
|
|
.iter()
|
|
.map(|(label, relative_offset)| {
|
|
build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
let post_capacity_dword_candidates = SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS
|
|
.iter()
|
|
.map(|(label, relative_offset)| {
|
|
build_save_dword_candidate(&bytes, record_offset, label, *relative_offset)
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
let stat_band_root_0cfb_candidates = build_save_company_stat_band_candidates(
|
|
&bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET,
|
|
"stat_band_0cfb",
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
|
|
)?;
|
|
let stat_band_root_0d7f_candidates = build_save_company_stat_band_candidates(
|
|
&bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET,
|
|
"stat_band_0d7f",
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
|
|
)?;
|
|
let stat_band_root_1c47_candidates = build_save_company_stat_band_candidates(
|
|
&bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET,
|
|
"stat_band_1c47",
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
|
|
)?;
|
|
entries.push(SmpSaveCompanyRecordAnalysisEntry {
|
|
company_id,
|
|
name,
|
|
active,
|
|
linked_chairman_profile_id,
|
|
outstanding_shares,
|
|
debt,
|
|
bond_count,
|
|
live_bond_slots,
|
|
largest_live_bond_principal,
|
|
highest_coupon_live_bond_principal,
|
|
available_track_laying_capacity,
|
|
company_value_scalar_f32,
|
|
cached_share_support_scalar_f32,
|
|
cached_share_price_f32,
|
|
chairman_salary_baseline,
|
|
chairman_salary_current,
|
|
chairman_bonus_year,
|
|
chairman_bonus_amount,
|
|
founding_year,
|
|
last_bankruptcy_year,
|
|
last_dividend_year,
|
|
preferred_locomotive_engine_type_raw_u8,
|
|
preferred_locomotive_engine_type_raw_hex: format!(
|
|
"0x{preferred_locomotive_engine_type_raw_u8:02x}"
|
|
),
|
|
city_connection_latch,
|
|
linked_transit_latch,
|
|
merger_cooldown_year,
|
|
takeover_cooldown_year,
|
|
scalar_dword_candidates,
|
|
post_capacity_dword_candidates,
|
|
stat_band_root_0cfb_candidates,
|
|
stat_band_root_0d7f_candidates,
|
|
stat_band_root_1c47_candidates,
|
|
});
|
|
}
|
|
entries
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
let company_share_prices = company_entries
|
|
.iter()
|
|
.filter_map(|entry| {
|
|
round_f64_to_i64(entry.cached_share_price_f32 as f64)
|
|
.map(|share_price| (entry.company_id, share_price))
|
|
})
|
|
.collect::<BTreeMap<_, _>>();
|
|
|
|
let chairman_entries = if let Some(header_probe) = chairman_header_probe {
|
|
let record_start_offset =
|
|
detect_save_chairman_profile_record_start_offset(&bytes, header_probe)?;
|
|
let record_stride = header_probe.direct_record_stride as usize;
|
|
let base_offset = header_probe
|
|
.metadata_tag_offset
|
|
.checked_add(4)?
|
|
.checked_add(record_start_offset)?;
|
|
let company_id_bound = company_header_probe
|
|
.map(|probe| probe.live_id_bound)
|
|
.unwrap_or(0);
|
|
let mut entries = Vec::with_capacity(header_probe.live_record_count as usize);
|
|
for index in 0..header_probe.live_record_count as usize {
|
|
let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?;
|
|
let profile_id = read_u32_at(&bytes, record_offset)?;
|
|
let active = read_u32_at(&bytes, record_offset + 4)? != 0;
|
|
let name = read_ascii_c_string_at(
|
|
&bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET,
|
|
SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN,
|
|
)?;
|
|
let current_cash =
|
|
read_f64_at(&bytes, record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET)?;
|
|
let linked_company_id = parse_nonzero_u32(
|
|
&bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET,
|
|
)?;
|
|
let personality_byte_0x291 = read_u8_at(
|
|
&bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET,
|
|
)?;
|
|
let mut holdings_by_company = BTreeMap::new();
|
|
for company_id in 1..=company_id_bound {
|
|
let slot_offset = record_offset
|
|
.checked_add(SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET)?
|
|
.checked_add((company_id as usize).checked_mul(4)?)?;
|
|
let units = read_u32_at(&bytes, slot_offset)?;
|
|
if units != 0 {
|
|
holdings_by_company.insert(company_id, units);
|
|
}
|
|
}
|
|
let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS
|
|
.iter()
|
|
.map(|relative_offset| {
|
|
build_save_qword_candidate(&bytes, record_offset, *relative_offset)
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
let rounded_current_cash = round_f64_to_i64(current_cash)?;
|
|
let derived_holdings_share_price_total = derive_chairman_holdings_share_price_total(
|
|
&holdings_by_company,
|
|
&company_share_prices,
|
|
);
|
|
let derived_net_worth_share_price_total = derived_holdings_share_price_total
|
|
.and_then(|holdings_total| rounded_current_cash.checked_add(holdings_total));
|
|
let derived_cached_purchasing_power_total =
|
|
derive_chairman_cached_purchasing_power_total(
|
|
rounded_current_cash,
|
|
&cached_scalar_candidates,
|
|
);
|
|
entries.push(SmpSaveChairmanRecordAnalysisEntry {
|
|
profile_id,
|
|
name,
|
|
active,
|
|
current_cash,
|
|
linked_company_id,
|
|
holdings_by_company,
|
|
derived_holdings_share_price_total,
|
|
derived_net_worth_share_price_total,
|
|
derived_cached_purchasing_power_total,
|
|
personality_byte_0x291,
|
|
personality_byte_0x291_hex: format!("0x{personality_byte_0x291:02x}"),
|
|
cached_scalar_candidates,
|
|
});
|
|
}
|
|
entries
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
let mut notes = Vec::new();
|
|
if world_selection_context.is_some() {
|
|
notes.push(
|
|
"World selection context now exports the grounded chairman-slot selector bytes and per-slot role-gate bytes from the fixed save-side 0x32c8 world block."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if world_issue_37.is_some() {
|
|
notes.push(
|
|
"World analysis now also exports the grounded issue-0x37 pair from the same 0x32c8 world payload: the clamped small issue value at [world+0x2d] and its companion multiplier lane at [world+0x29]."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if world_economic_tuning.is_some() {
|
|
notes.push(
|
|
"World analysis now also exports the fixed six-lane economic tuning float block from the same 0x32c8 world payload; current atlas evidence still treats that band as distinct from the issue-0x37 investor-confidence family."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if world_finance_neighborhood.is_some() {
|
|
notes.push(
|
|
"World analysis now also exports one fixed dword finance neighborhood around the grounded issue/calendar lanes, so future issue-0x38/0x39 closure can build on rehosted owner-state candidates instead of ad hoc byte guesses."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if let Some(header) = report.save_train_collection_header_probe.as_ref() {
|
|
notes.push(format!(
|
|
"Train analysis now also exports the tagged train collection header: live_record_count={} live_id_bound={} direct_record_stride=0x{:x}.",
|
|
header.live_record_count, header.live_id_bound, header.direct_record_stride
|
|
));
|
|
}
|
|
if let Some(directory) = train_collection_directory.as_ref() {
|
|
notes.push(format!(
|
|
"Train analysis now also exports the tagged live-entry directory rooted at metadata dword {}: {} entries chained from {:?} to {:?}.",
|
|
directory.directory_root_dword_index,
|
|
directory.entries.len(),
|
|
directory.chain_head_live_entry_id,
|
|
directory.chain_tail_live_entry_id
|
|
));
|
|
}
|
|
if let Some(header) = report.save_region_collection_header_probe.as_ref() {
|
|
notes.push(format!(
|
|
"Region analysis now also exports the non-direct tagged region collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.",
|
|
header.live_record_count, header.live_id_bound, header.direct_record_stride
|
|
));
|
|
}
|
|
if let Some(triplets) = region_record_triplets.as_ref() {
|
|
notes.push(format!(
|
|
"Region analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first serialized region name={:?}, first policy lanes=({:.3}, {:.3}, {:.3}), first profile collection count={:?}, first profile collection trailing_padding_len={:?}.",
|
|
triplets.record_count,
|
|
triplets.entries.first().map(|entry| entry.name.as_str()),
|
|
triplets
|
|
.entries
|
|
.first()
|
|
.map(|entry| entry.policy_leading_f32_0)
|
|
.unwrap_or_default(),
|
|
triplets
|
|
.entries
|
|
.first()
|
|
.map(|entry| entry.policy_leading_f32_1)
|
|
.unwrap_or_default(),
|
|
triplets
|
|
.entries
|
|
.first()
|
|
.map(|entry| entry.policy_leading_f32_2)
|
|
.unwrap_or_default(),
|
|
triplets.entries.first().and_then(|entry| {
|
|
entry.profile_collection.as_ref().map(|collection| collection.live_record_count)
|
|
}),
|
|
triplets.entries.first().and_then(|entry| {
|
|
entry.profile_collection.as_ref().map(|collection| collection.trailing_padding_len)
|
|
})
|
|
));
|
|
}
|
|
if let Some(queue_probe) = region_queued_notice_records.as_ref() {
|
|
notes.push(format!(
|
|
"Region analysis now also exports {} queued kind-7 notice nodes with payload seed {}: first region id={} amount={} promotion={} tails={}/{}.",
|
|
queue_probe.entries.len(),
|
|
queue_probe.payload_seed_dword_hex,
|
|
queue_probe.entries[0].region_id,
|
|
queue_probe.entries[0].amount,
|
|
queue_probe.entries[0].promotion_latch_dword_hex,
|
|
queue_probe.entries[0].trailing_sentinel_i32_0_hex,
|
|
queue_probe.entries[0].trailing_sentinel_i32_1_hex
|
|
));
|
|
}
|
|
if let Some(header) = report
|
|
.save_placed_structure_collection_header_probe
|
|
.as_ref()
|
|
{
|
|
notes.push(format!(
|
|
"Placed-structure analysis now also exports the tagged collection header: live_record_count={} live_id_bound={} serialized_stride_hint=0x{:x}.",
|
|
header.live_record_count, header.live_id_bound, header.direct_record_stride
|
|
));
|
|
}
|
|
if let Some(triplets) = placed_structure_record_triplets.as_ref() {
|
|
notes.push(format!(
|
|
"Placed-structure analysis now also exports {} tagged 0x55f1/0x55f2/0x55f3 record triplets; first stems={:?}/{:?}, first footer payload={}, first footer status kind={:?}.",
|
|
triplets.record_count,
|
|
triplets.entries.first().map(|entry| entry.primary_name.as_str()),
|
|
triplets.entries.first().map(|entry| entry.secondary_name.as_str()),
|
|
triplets.entries.first().map(|entry| entry.profile_payload_dword_hex.as_str()).unwrap_or("0x00000000"),
|
|
triplets.entries.first().map(|entry| entry.profile_status_kind.as_str())
|
|
));
|
|
}
|
|
if let Some(side_buffer) = placed_structure_dynamic_side_buffer.as_ref() {
|
|
let dominant_pattern = side_buffer.compact_prefix_pattern_summaries.first();
|
|
notes.push(format!(
|
|
"Placed-structure analysis now also exports the separate 0x38a5 dynamic side-buffer owner seam with {} embedded name rows, {} decoded rows across {} unique name pairs, {} rows with a tertiary 0x55f1 string, {} unique compact prefix patterns, {} rows whose leading dword matches 0x55f3, and dominant compact pattern={}/{}/{} x{}.",
|
|
side_buffer.embedded_name_tag_count,
|
|
side_buffer.decoded_embedded_name_row_count,
|
|
side_buffer.unique_embedded_name_pair_count,
|
|
side_buffer.decoded_embedded_name_row_with_tertiary_name_count,
|
|
side_buffer.unique_compact_prefix_pattern_count,
|
|
side_buffer.prefix_leading_dword_matching_embedded_profile_tag_count,
|
|
dominant_pattern
|
|
.map(|pattern| pattern.prefix_leading_dword_hex.as_str())
|
|
.unwrap_or("0x00000000"),
|
|
dominant_pattern
|
|
.map(|pattern| pattern.prefix_trailing_word_hex.as_str())
|
|
.unwrap_or("0x0000"),
|
|
dominant_pattern
|
|
.map(|pattern| pattern.prefix_separator_byte_hex.as_str())
|
|
.unwrap_or("0x00"),
|
|
dominant_pattern.map(|pattern| pattern.count).unwrap_or_default()
|
|
));
|
|
}
|
|
if let Some(alignment) = placed_structure_dynamic_side_buffer_alignment.as_ref() {
|
|
notes.push(format!(
|
|
"Placed-structure analysis now also compares the 0x38a5 side-buffer against the grounded 0x36b1 triplet corpus: {} of {} decoded side-buffer rows reuse {} overlapping placed-structure name pairs, leaving {} unmatched side-buffer rows and {} triplet-only name pairs.",
|
|
alignment.side_buffer_rows_with_matching_triplet_name_pair_count,
|
|
alignment.side_buffer_row_count,
|
|
alignment.overlapping_name_pair_count,
|
|
alignment.side_buffer_rows_without_matching_triplet_name_pair_count,
|
|
alignment.triplet_name_pairs_without_side_buffer_match_count
|
|
));
|
|
}
|
|
if let Some(candidate) = unclassified_tagged_collection_headers.first() {
|
|
notes.push(format!(
|
|
"Generic save-side tagged collection scan also found {} unclassified candidate families; largest current candidate uses tags {}/{}/{} with live_record_count={} stride=0x{:x} records_span_len=0x{:x}.",
|
|
unclassified_tagged_collection_headers.len(),
|
|
candidate.metadata_tag_hex,
|
|
candidate.records_tag_hex,
|
|
candidate.close_tag_hex,
|
|
candidate.live_record_count,
|
|
candidate.direct_record_stride,
|
|
candidate.records_span_len
|
|
));
|
|
}
|
|
if !company_entries.is_empty() {
|
|
notes.push(
|
|
"Company debt is derived from the grounded bond table at [company+0x5b/+0x5f] by summing live principal slots.".to_string(),
|
|
);
|
|
notes.push(
|
|
"Company available_track_laying_capacity is derived from the grounded tail dword [company+0x7680], with negative values treated as the unlimited sentinel.".to_string(),
|
|
);
|
|
notes.push(
|
|
"Company scalar_dword_candidates expose the current checked-in raw save windows around support/share-price/calendar lanes, and post_capacity_dword_candidates expose the immediate dwords after [company+0x7680] for deeper track-count and record-tail analysis.".to_string(),
|
|
);
|
|
notes.push(
|
|
"Company stat-band root candidates now also expose the first dword windows rooted at [company+0x0cfb], [company+0x0d7f], and [company+0x1c47], the same broader stat bands the grounded cheat reset branch clears before later finance/detail readers rebuild them.".to_string(),
|
|
);
|
|
notes.push(
|
|
"Current atlas evidence ties company current_cash and book_value_per_share to stat-family 0x2329 slots 0x0d and 0x1d, so the remaining save-native company finance/governance closure likely needs a structured company-stat family reconstruction instead of more isolated raw offsets."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if !chairman_entries.is_empty() {
|
|
notes.push(
|
|
"Chairman cached_scalar_candidates expose the adjacent qword band rooted at [profile+0x1e9], now including raw qword hex and signed/f64 views for further purchasing-power analysis.".to_string(),
|
|
);
|
|
notes.push(
|
|
"Chairman analysis now also derives one holdings-at-cached-share-price total from the grounded company cached_share_price lane and one strongest-cached purchasing-power total from the nonnegative qword cache band."
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
Some(SmpSaveCompanyChairmanAnalysisReport {
|
|
profile_family: report
|
|
.container_profile
|
|
.as_ref()
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string()),
|
|
selected_company_id: selection_probe.map(|probe| probe.selected_company_id),
|
|
selected_chairman_profile_id: selection_probe
|
|
.map(|probe| probe.selected_chairman_profile_id),
|
|
world_selection_context,
|
|
world_issue_37,
|
|
world_economic_tuning,
|
|
world_finance_neighborhood,
|
|
train_collection_header: report.save_train_collection_header_probe.clone(),
|
|
train_collection_directory,
|
|
region_collection_header: report.save_region_collection_header_probe.clone(),
|
|
region_record_triplets,
|
|
region_queued_notice_records,
|
|
placed_structure_collection_header: report
|
|
.save_placed_structure_collection_header_probe
|
|
.clone(),
|
|
placed_structure_record_triplets,
|
|
placed_structure_dynamic_side_buffer,
|
|
placed_structure_dynamic_side_buffer_alignment,
|
|
unclassified_tagged_collection_headers,
|
|
company_entries,
|
|
chairman_entries,
|
|
notes,
|
|
})
|
|
}
|
|
|
|
fn derive_locomotive_catalog_from_named_availability_table(
|
|
table: &SmpLoadedNamedLocomotiveAvailabilityTable,
|
|
) -> Option<SmpLoadedLocomotiveCatalog> {
|
|
if table.entries.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let entries = table
|
|
.entries
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, entry)| SmpLoadedLocomotiveCatalogEntry {
|
|
locomotive_id: (index + 1) as u32,
|
|
name: entry.text.clone(),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
Some(SmpLoadedLocomotiveCatalog {
|
|
source_kind: format!("{}-ordinal-catalog", table.source_kind),
|
|
semantic_family: "scenario-save-derived-locomotive-catalog".to_string(),
|
|
entries_offset: table.entries_offset,
|
|
observed_entry_count: entries.len(),
|
|
entries,
|
|
})
|
|
}
|
|
|
|
fn derive_cargo_catalog_from_recipe_book_probe(
|
|
probe: &SmpRecipeBookSummaryProbe,
|
|
) -> Option<SmpLoadedCargoCatalog> {
|
|
if probe.books.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let entries = probe
|
|
.books
|
|
.iter()
|
|
.filter(|book| book.book_index < 11)
|
|
.filter_map(|book| {
|
|
let line = book
|
|
.lines
|
|
.iter()
|
|
.find(|line| line.imports_to_runtime_descriptor)
|
|
.or_else(|| book.lines.first())?;
|
|
let slot_id = (book.book_index + 1) as u32;
|
|
let definition = known_cargo_slot_definition(slot_id)?;
|
|
Some(SmpLoadedCargoCatalogEntry {
|
|
slot_id,
|
|
label: definition.label.to_string(),
|
|
cargo_class: definition.cargo_class,
|
|
book_index: book.book_index,
|
|
max_annual_production_word: book.max_annual_production_word,
|
|
mode_word: line.mode_word,
|
|
runtime_import_branch_kind: line.runtime_import_branch_kind.clone(),
|
|
annual_amount_word: line.annual_amount_word,
|
|
supplied_cargo_token_word: line.supplied_cargo_token_word,
|
|
supplied_cargo_token_probable_high16_ascii_stem: line
|
|
.supplied_cargo_token_probable_high16_ascii_stem
|
|
.clone(),
|
|
demanded_cargo_token_word: line.demanded_cargo_token_word,
|
|
demanded_cargo_token_probable_high16_ascii_stem: line
|
|
.demanded_cargo_token_probable_high16_ascii_stem
|
|
.clone(),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
if entries.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some(SmpLoadedCargoCatalog {
|
|
source_kind: format!("{}-slot-catalog", probe.source_kind),
|
|
semantic_family: "scenario-save-derived-cargo-catalog".to_string(),
|
|
root_offset: Some(probe.root_offset),
|
|
observed_entry_count: entries.len(),
|
|
entries,
|
|
})
|
|
}
|
|
|
|
fn derive_loaded_world_issue_37_state_from_probe(
|
|
probe: &SmpSaveWorldIssue37Probe,
|
|
) -> SmpLoadedWorldIssue37State {
|
|
SmpLoadedWorldIssue37State {
|
|
source_kind: probe.source_kind.clone(),
|
|
semantic_family: probe.semantic_family.clone(),
|
|
issue_value: probe.issue_value_lane.raw_u32,
|
|
issue_value_hex: probe.issue_value_lane.raw_u32_hex.clone(),
|
|
issue_38_value: u32::from(probe.issue_38_raw_u8),
|
|
issue_38_value_hex: probe.issue_38_raw_hex.clone(),
|
|
issue_39_value: u32::from(probe.issue_39_raw_u8),
|
|
issue_39_value_hex: probe.issue_39_raw_hex.clone(),
|
|
issue_3a_value: u32::from(probe.issue_3a_raw_u8),
|
|
issue_3a_value_hex: probe.issue_3a_raw_hex.clone(),
|
|
multiplier_raw_u32: probe.multiplier_lane.raw_u32,
|
|
multiplier_raw_hex: probe.multiplier_lane.raw_u32_hex.clone(),
|
|
multiplier_value_f32_text: format!("{:.6}", probe.multiplier_lane.value_f32),
|
|
issue_opinion_base_terms_raw_i32: probe.issue_opinion_base_terms_raw_i32.clone(),
|
|
}
|
|
}
|
|
|
|
fn derive_loaded_world_economic_tuning_state_from_probe(
|
|
probe: &SmpSaveWorldEconomicTuningProbe,
|
|
) -> SmpLoadedWorldEconomicTuningState {
|
|
SmpLoadedWorldEconomicTuningState {
|
|
source_kind: probe.source_kind.clone(),
|
|
semantic_family: probe.semantic_family.clone(),
|
|
mirror_raw_u32: probe.mirror_lane.raw_u32,
|
|
mirror_raw_hex: probe.mirror_lane.raw_u32_hex.clone(),
|
|
mirror_value_f32_text: format!("{:.6}", probe.mirror_lane.value_f32),
|
|
lane_raw_u32: probe.tuning_lanes.iter().map(|lane| lane.raw_u32).collect(),
|
|
lane_raw_hex: probe
|
|
.tuning_lanes
|
|
.iter()
|
|
.map(|lane| lane.raw_u32_hex.clone())
|
|
.collect(),
|
|
lane_value_f32_text: probe
|
|
.tuning_lanes
|
|
.iter()
|
|
.map(|lane| format!("{:.6}", lane.value_f32))
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn derive_loaded_world_finance_neighborhood_state_from_probe(
|
|
probe: &SmpSaveWorldFinanceNeighborhoodProbe,
|
|
) -> SmpLoadedWorldFinanceNeighborhoodState {
|
|
SmpLoadedWorldFinanceNeighborhoodState {
|
|
source_kind: probe.source_kind.clone(),
|
|
semantic_family: probe.semantic_family.clone(),
|
|
packed_year_word_raw_u16: probe.packed_year_word_raw_u16,
|
|
packed_year_word_raw_hex: probe.packed_year_word_raw_hex.clone(),
|
|
partial_year_progress_raw_u8: probe.partial_year_progress_raw_u8,
|
|
partial_year_progress_raw_hex: probe.partial_year_progress_raw_hex.clone(),
|
|
current_calendar_tuple_word_raw_u32: probe.current_calendar_tuple_word_lane.raw_u32,
|
|
current_calendar_tuple_word_raw_hex: probe
|
|
.current_calendar_tuple_word_lane
|
|
.raw_u32_hex
|
|
.clone(),
|
|
current_calendar_tuple_word_2_raw_u32: probe.current_calendar_tuple_word_2_lane.raw_u32,
|
|
current_calendar_tuple_word_2_raw_hex: probe
|
|
.current_calendar_tuple_word_2_lane
|
|
.raw_u32_hex
|
|
.clone(),
|
|
absolute_counter_raw_u32: probe.absolute_counter_lane.raw_u32,
|
|
absolute_counter_raw_hex: probe.absolute_counter_lane.raw_u32_hex.clone(),
|
|
absolute_counter_mirror_raw_u32: probe.absolute_counter_mirror_lane.raw_u32,
|
|
absolute_counter_mirror_raw_hex: probe.absolute_counter_mirror_lane.raw_u32_hex.clone(),
|
|
stock_policy_raw_u8: probe.stock_policy_raw_u8,
|
|
stock_policy_raw_hex: probe.stock_policy_raw_hex.clone(),
|
|
bond_policy_raw_u8: probe.bond_policy_raw_u8,
|
|
bond_policy_raw_hex: probe.bond_policy_raw_hex.clone(),
|
|
bankruptcy_policy_raw_u8: probe.bankruptcy_policy_raw_u8,
|
|
bankruptcy_policy_raw_hex: probe.bankruptcy_policy_raw_hex.clone(),
|
|
dividend_policy_raw_u8: probe.dividend_policy_raw_u8,
|
|
dividend_policy_raw_hex: probe.dividend_policy_raw_hex.clone(),
|
|
building_density_growth_setting_raw_u32: probe.building_density_growth_setting_lane.raw_u32,
|
|
building_density_growth_setting_raw_hex: probe
|
|
.building_density_growth_setting_lane
|
|
.raw_u32_hex
|
|
.clone(),
|
|
labels: probe
|
|
.dword_candidates
|
|
.iter()
|
|
.map(|candidate| candidate.label.clone())
|
|
.collect(),
|
|
relative_offsets: probe
|
|
.dword_candidates
|
|
.iter()
|
|
.map(|candidate| candidate.relative_offset)
|
|
.collect(),
|
|
relative_offset_hex: probe
|
|
.dword_candidates
|
|
.iter()
|
|
.map(|candidate| candidate.relative_offset_hex.clone())
|
|
.collect(),
|
|
raw_u32: probe
|
|
.dword_candidates
|
|
.iter()
|
|
.map(|candidate| candidate.raw_u32)
|
|
.collect(),
|
|
raw_hex: probe
|
|
.dword_candidates
|
|
.iter()
|
|
.map(|candidate| candidate.raw_u32_hex.clone())
|
|
.collect(),
|
|
value_i32: probe
|
|
.dword_candidates
|
|
.iter()
|
|
.map(|candidate| candidate.value_i32)
|
|
.collect(),
|
|
value_f32_text: probe
|
|
.dword_candidates
|
|
.iter()
|
|
.map(|candidate| format!("{:.6}", candidate.value_f32))
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn derive_loaded_world_locomotive_policy_state_from_probes(
|
|
post_text_probe: Option<&SmpPostTextFieldNeighborhoodProbe>,
|
|
locomotive_policy_probe: Option<&SmpLocomotivePolicyNeighborhoodProbe>,
|
|
) -> Option<SmpLoadedWorldLocomotivePolicyState> {
|
|
let field_by_name = |name: &str| {
|
|
locomotive_policy_probe?
|
|
.grounded_field_observations
|
|
.iter()
|
|
.find(|field| field.field_name == name)
|
|
};
|
|
let post_text_field_by_name = |name: &str| {
|
|
post_text_probe?
|
|
.grounded_field_observations
|
|
.iter()
|
|
.find(|field| field.field_name == name)
|
|
};
|
|
let selected_year_gap_scalar = field_by_name("selected-year bucket companion scalar");
|
|
let linked_site_gate = field_by_name("linked-site removal follow-on gate");
|
|
let auto_show_grade = post_text_field_by_name("Auto-Show Grade During Track Lay");
|
|
let starting_building_density = post_text_field_by_name("Starting Building Density Level");
|
|
let building_density_growth = post_text_field_by_name("Building Density Growth");
|
|
let leftover_simulation_time = post_text_field_by_name("leftover simulation time accumulator");
|
|
let selected_year_snapshot = post_text_field_by_name("selected-year lane snapshot");
|
|
let all_steam = field_by_name("All Steam Locos Avail.");
|
|
let all_diesel = field_by_name("All Diesel Locos Avail.");
|
|
let all_electric = field_by_name("All Electric Locos Avail.");
|
|
let cached_available_rating = field_by_name("cached available-locomotive rating");
|
|
Some(SmpLoadedWorldLocomotivePolicyState {
|
|
source_kind: locomotive_policy_probe
|
|
.map(|probe| probe.source_kind.clone())
|
|
.or_else(|| post_text_probe.map(|probe| probe.source_kind.clone()))?,
|
|
semantic_family: "world-locomotive-policy".to_string(),
|
|
selected_year_gap_scalar_raw_u32: selected_year_gap_scalar
|
|
.and_then(|field| field.value_u32),
|
|
selected_year_gap_scalar_raw_hex: selected_year_gap_scalar
|
|
.and_then(|field| field.value_u32_hex.clone()),
|
|
selected_year_gap_scalar_value_f32_text: selected_year_gap_scalar
|
|
.and_then(|field| field.probable_f32_le.clone()),
|
|
linked_site_removal_follow_on_gate_raw_u8: linked_site_gate
|
|
.and_then(|field| field.value_u8),
|
|
linked_site_removal_follow_on_gate_raw_hex: linked_site_gate
|
|
.and_then(|field| field.value_u8_hex.clone()),
|
|
auto_show_grade_during_track_lay_raw_u8: auto_show_grade.and_then(|field| field.value_u8),
|
|
auto_show_grade_during_track_lay_raw_hex: auto_show_grade
|
|
.and_then(|field| field.value_u8_hex.clone()),
|
|
starting_building_density_level_raw_u8: starting_building_density
|
|
.and_then(|field| field.value_u8),
|
|
starting_building_density_level_raw_hex: starting_building_density
|
|
.and_then(|field| field.value_u8_hex.clone()),
|
|
building_density_growth_raw_u8: building_density_growth.and_then(|field| field.value_u8),
|
|
building_density_growth_raw_hex: building_density_growth
|
|
.and_then(|field| field.value_u8_hex.clone()),
|
|
leftover_simulation_time_accumulator_raw_u32: leftover_simulation_time
|
|
.and_then(|field| field.value_u32),
|
|
leftover_simulation_time_accumulator_raw_hex: leftover_simulation_time
|
|
.and_then(|field| field.value_u32_hex.clone()),
|
|
leftover_simulation_time_accumulator_value_f32_text: leftover_simulation_time
|
|
.and_then(|field| field.probable_f32_le.clone()),
|
|
selected_year_lane_snapshot_raw_u8: selected_year_snapshot.and_then(|field| field.value_u8),
|
|
selected_year_lane_snapshot_raw_hex: selected_year_snapshot
|
|
.and_then(|field| field.value_u8_hex.clone()),
|
|
all_steam_locomotives_available_raw_u8: all_steam.and_then(|field| field.value_u8),
|
|
all_steam_locomotives_available_raw_hex: all_steam
|
|
.and_then(|field| field.value_u8_hex.clone()),
|
|
all_diesel_locomotives_available_raw_u8: all_diesel.and_then(|field| field.value_u8),
|
|
all_diesel_locomotives_available_raw_hex: all_diesel
|
|
.and_then(|field| field.value_u8_hex.clone()),
|
|
all_electric_locomotives_available_raw_u8: all_electric.and_then(|field| field.value_u8),
|
|
all_electric_locomotives_available_raw_hex: all_electric
|
|
.and_then(|field| field.value_u8_hex.clone()),
|
|
cached_available_locomotive_rating_raw_u32: cached_available_rating
|
|
.and_then(|field| field.value_u32),
|
|
cached_available_locomotive_rating_raw_hex: cached_available_rating
|
|
.and_then(|field| field.value_u32_hex.clone()),
|
|
cached_available_locomotive_rating_value_f32_text: cached_available_rating
|
|
.and_then(|field| field.probable_f32_le.clone()),
|
|
})
|
|
}
|
|
|
|
fn derive_selection_only_company_roster_from_save_world_probe(
|
|
probe: &SmpSaveWorldSelectionContextProbe,
|
|
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
) -> Option<SmpLoadedCompanyRoster> {
|
|
Some(SmpLoadedCompanyRoster {
|
|
source_kind: format!("{}-company-selection-only", probe.source_kind),
|
|
semantic_family: "scenario-selected-company-context".to_string(),
|
|
observed_entry_count: header_probe
|
|
.map(|probe| probe.live_record_count as usize)
|
|
.unwrap_or(0),
|
|
selected_company_id: Some(probe.selected_company_id),
|
|
entries: Vec::new(),
|
|
})
|
|
}
|
|
|
|
fn derive_selection_only_chairman_profile_table_from_save_world_probe(
|
|
probe: &SmpSaveWorldSelectionContextProbe,
|
|
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
) -> Option<SmpLoadedChairmanProfileTable> {
|
|
Some(SmpLoadedChairmanProfileTable {
|
|
source_kind: format!("{}-chairman-selection-only", probe.source_kind),
|
|
semantic_family: "scenario-selected-chairman-context".to_string(),
|
|
observed_entry_count: header_probe
|
|
.map(|probe| probe.live_record_count as usize)
|
|
.unwrap_or(0),
|
|
selected_chairman_profile_id: Some(probe.selected_chairman_profile_id),
|
|
entries: Vec::new(),
|
|
})
|
|
}
|
|
|
|
const SAVE_COMPANY_RECORD_NAME_OFFSET: usize = 0x04;
|
|
const SAVE_COMPANY_RECORD_NAME_MAX_LEN: usize = 0x24;
|
|
const SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET: usize = 0x3b;
|
|
const SAVE_COMPANY_RECORD_ACTIVE_OFFSET: usize = 0x3f;
|
|
const SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET: usize = 0x47;
|
|
const SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET: usize = 0x4f;
|
|
const SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET: usize = 0x57;
|
|
const SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET: usize = 0x5b;
|
|
const SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET: usize = 0x5f;
|
|
const SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE: usize = 12;
|
|
const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET: usize = 0x14f;
|
|
const SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET: usize = 0x34f;
|
|
const SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET: usize = 0x353;
|
|
const SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET: usize = 0x15f;
|
|
const SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET: usize = 0x157;
|
|
const SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163;
|
|
const SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET: usize = 0x16b;
|
|
const SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2: usize = 0x16f;
|
|
const SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET: usize = 0x173;
|
|
const SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2: usize = 0x177;
|
|
const SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET: usize = 0x289;
|
|
const SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET: usize = 0x0d2d;
|
|
const SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET: usize = 0x0d17;
|
|
const SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18;
|
|
const SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET: usize = 0x0d07;
|
|
const SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET: usize = 0x0d59;
|
|
const SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56;
|
|
const SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET: usize = 0x0d19;
|
|
const SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET: usize = 0x0d7b;
|
|
const SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET: usize = 0x7680;
|
|
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET: usize = 0x0cfb;
|
|
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET: usize = 0x0d7f;
|
|
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET: usize = 0x1c47;
|
|
const SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS: usize = 32;
|
|
const SAVE_COMPANY_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x2ab;
|
|
const SAVE_COMPANY_RECORD_ISSUE_OPINION_TERM_COUNT: usize =
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT;
|
|
const SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT: usize =
|
|
crate::runtime::RUNTIME_COMPANY_STAT_SLOT_COUNT as usize
|
|
* crate::runtime::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN as usize;
|
|
const SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT: usize =
|
|
crate::runtime::RUNTIME_COMPANY_STAT_SLOT_COUNT as usize;
|
|
const SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_FLOAT_FIELDS: [usize; 10] = [
|
|
0x4b, 0x53, 0x323, 0x327, 0x32b, 0x32f, 0x333, 0x337, 0x33b, 0x33f,
|
|
];
|
|
const SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_INT_FIELDS: [usize; 5] =
|
|
[0x14f, 0x34b, 0x0d0b, 0x0d0f, 0x0d13];
|
|
const SAVE_COMPANY_RECORD_SCALAR_CANDIDATE_FIELDS: [(&str, usize); 9] = [
|
|
("mutable_support_scalar", 0x4f),
|
|
("young_company_support_scalar", 0x57),
|
|
(
|
|
"support_progress_word",
|
|
SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET,
|
|
),
|
|
(
|
|
"recent_per_share_subscore",
|
|
SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET,
|
|
),
|
|
("cached_share_price", 0x0d7b),
|
|
(
|
|
"current_issue_calendar_word",
|
|
SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET,
|
|
),
|
|
(
|
|
"current_issue_calendar_word_2",
|
|
SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2,
|
|
),
|
|
(
|
|
"prior_issue_calendar_word",
|
|
SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET,
|
|
),
|
|
(
|
|
"prior_issue_calendar_word_2",
|
|
SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2,
|
|
),
|
|
];
|
|
const SAVE_COMPANY_RECORD_POST_CAPACITY_CANDIDATE_FIELDS: [(&str, usize); 6] = [
|
|
("post_capacity_word_1", 0x7684),
|
|
("post_capacity_word_2", 0x7688),
|
|
("post_capacity_word_3", 0x768c),
|
|
("post_capacity_word_4", 0x7690),
|
|
("post_capacity_word_5", 0x7694),
|
|
("post_capacity_word_6", 0x7698),
|
|
];
|
|
const SAVE_COMPANY_RECORD_START_SCAN_LIMIT: usize = 0x120;
|
|
|
|
const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08;
|
|
const SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN: usize = 0x1f;
|
|
const SAVE_CHAIRMAN_RECORD_CASH_OFFSET: usize = 0x154;
|
|
const SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET: usize = 0x15d;
|
|
const SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET: usize = 0x1dd;
|
|
const SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET: usize = 0x1e9;
|
|
const SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET: usize = 0x1f1;
|
|
const SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET: usize = 0x291;
|
|
const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x35b;
|
|
const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT: usize =
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT;
|
|
const SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS: [usize; 7] =
|
|
[0x1e9, 0x1f1, 0x1f9, 0x201, 0x209, 0x211, 0x219];
|
|
const SAVE_CHAIRMAN_RECORD_START_SCAN_LIMIT: usize = 0x80;
|
|
|
|
fn parse_save_company_roster_probe(
|
|
bytes: &[u8],
|
|
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
selection_probe: Option<&SmpSaveWorldSelectionContextProbe>,
|
|
) -> Option<SmpLoadedCompanyRoster> {
|
|
let header_probe = header_probe?;
|
|
let observed_entry_count = header_probe.live_record_count as usize;
|
|
if observed_entry_count == 0 {
|
|
return Some(SmpLoadedCompanyRoster {
|
|
source_kind: "save-company-direct-records".to_string(),
|
|
semantic_family: "scenario-save-company-direct-records".to_string(),
|
|
observed_entry_count,
|
|
selected_company_id: selection_probe.map(|probe| probe.selected_company_id),
|
|
entries: Vec::new(),
|
|
});
|
|
}
|
|
|
|
let record_start_offset = detect_save_company_record_start_offset(bytes, header_probe)?;
|
|
let record_stride = header_probe.direct_record_stride as usize;
|
|
let base_offset = header_probe
|
|
.metadata_tag_offset
|
|
.checked_add(4)?
|
|
.checked_add(record_start_offset)?;
|
|
|
|
let mut entries = Vec::with_capacity(observed_entry_count);
|
|
for index in 0..observed_entry_count {
|
|
let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?;
|
|
let company_id = read_u32_at(bytes, record_offset)?;
|
|
let active = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET)? != 0;
|
|
let linked_chairman_profile_id = parse_nonzero_u32(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET,
|
|
)?;
|
|
let outstanding_shares = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_OUTSTANDING_SHARES_OFFSET,
|
|
)?;
|
|
let debt = parse_save_company_total_debt(bytes, record_offset)?;
|
|
let bond_count = read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)?;
|
|
let live_bond_slots = parse_save_company_live_bond_slots(bytes, record_offset)?;
|
|
let largest_live_bond_principal =
|
|
parse_save_company_largest_live_bond_principal(bytes, record_offset)?;
|
|
let highest_coupon_live_bond_principal =
|
|
parse_save_company_highest_coupon_live_bond_principal(bytes, record_offset)?;
|
|
let available_track_laying_capacity =
|
|
parse_save_company_available_track_laying_capacity(bytes, record_offset)?;
|
|
let mutable_support_scalar_raw_u32 = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET,
|
|
)?;
|
|
let young_company_support_scalar_raw_u32 = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET,
|
|
)?;
|
|
let support_progress_word = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET,
|
|
)?;
|
|
let recent_per_share_cache_absolute_counter = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET,
|
|
)?;
|
|
let recent_per_share_cached_value_bits = read_u64_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET + 4,
|
|
)?;
|
|
let recent_per_share_subscore_raw_u32 = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET,
|
|
)?;
|
|
let cached_share_price_raw_u32 = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET,
|
|
)?;
|
|
let chairman_salary_baseline = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET,
|
|
)?;
|
|
let chairman_salary_current = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET,
|
|
)?;
|
|
let chairman_bonus_year = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET,
|
|
)?;
|
|
let chairman_bonus_amount = read_i32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET,
|
|
)?;
|
|
let founding_year = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET,
|
|
)?;
|
|
let last_bankruptcy_year = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET,
|
|
)?;
|
|
let last_dividend_year = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET,
|
|
)?;
|
|
let current_issue_calendar_word = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET,
|
|
)?;
|
|
let current_issue_calendar_word_2 = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2,
|
|
)?;
|
|
let prior_issue_calendar_word = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET,
|
|
)?;
|
|
let prior_issue_calendar_word_2 = read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2,
|
|
)?;
|
|
let preferred_locomotive_engine_type_raw_u8 = read_u8_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET,
|
|
)?;
|
|
let city_connection_latch = read_u8_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET,
|
|
)? != 0;
|
|
let linked_transit_latch = read_u8_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET,
|
|
)? != 0;
|
|
let merger_cooldown_year = parse_nonzero_u32(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET,
|
|
)?;
|
|
let takeover_cooldown_year = parse_nonzero_u32(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET,
|
|
)?;
|
|
let stat_band_root_0cfb_candidates = build_save_company_stat_band_candidates(
|
|
bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0CFB_OFFSET,
|
|
"stat_band_0cfb",
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
|
|
)?;
|
|
let stat_band_root_0d7f_candidates = build_save_company_stat_band_candidates(
|
|
bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET,
|
|
"stat_band_0d7f",
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
|
|
)?;
|
|
let stat_band_root_1c47_candidates = build_save_company_stat_band_candidates(
|
|
bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET,
|
|
"stat_band_1c47",
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS,
|
|
)?;
|
|
let year_stat_family_qword_bits = build_save_company_stat_qword_bits(
|
|
bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET,
|
|
SAVE_COMPANY_RECORD_YEAR_STAT_FAMILY_QWORD_COUNT,
|
|
)?;
|
|
let special_stat_family_232a_qword_bits = build_save_company_stat_qword_bits(
|
|
bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_1C47_OFFSET,
|
|
SAVE_COMPANY_RECORD_SPECIAL_STAT_FAMILY_232A_QWORD_COUNT,
|
|
)?;
|
|
let direct_control_transfer_float_fields_raw_u32 = build_save_u32_field_map(
|
|
bytes,
|
|
record_offset,
|
|
&SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_FLOAT_FIELDS,
|
|
)?;
|
|
let direct_control_transfer_int_fields_raw_u32 = build_save_u32_field_map(
|
|
bytes,
|
|
record_offset,
|
|
&SAVE_COMPANY_RECORD_DIRECT_CONTROL_TRANSFER_INT_FIELDS,
|
|
)?;
|
|
let issue_opinion_terms_raw_i32 = build_save_i32_term_strip(
|
|
bytes,
|
|
record_offset,
|
|
SAVE_COMPANY_RECORD_ISSUE_OPINION_TERMS_OFFSET,
|
|
SAVE_COMPANY_RECORD_ISSUE_OPINION_TERM_COUNT,
|
|
)?;
|
|
let current_cash = decode_save_company_current_year_stat_slot(
|
|
&year_stat_family_qword_bits,
|
|
crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
|
|
)
|
|
.and_then(round_f64_to_i64)
|
|
.unwrap_or(0);
|
|
entries.push(SmpLoadedCompanyRosterEntry {
|
|
company_id,
|
|
active,
|
|
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
|
current_cash,
|
|
debt,
|
|
credit_rating_score: None,
|
|
prime_rate: None,
|
|
available_track_laying_capacity,
|
|
track_piece_counts: RuntimeTrackPieceCounts::default(),
|
|
linked_chairman_profile_id,
|
|
book_value_per_share: 0,
|
|
investor_confidence: 0,
|
|
management_attitude: 0,
|
|
takeover_cooldown_year,
|
|
merger_cooldown_year,
|
|
preferred_locomotive_engine_type_raw_u8: Some(preferred_locomotive_engine_type_raw_u8),
|
|
market_state: Some(RuntimeCompanyMarketState {
|
|
outstanding_shares,
|
|
bond_count,
|
|
live_bond_slots,
|
|
largest_live_bond_principal,
|
|
highest_coupon_live_bond_principal,
|
|
mutable_support_scalar_raw_u32,
|
|
young_company_support_scalar_raw_u32,
|
|
support_progress_word,
|
|
recent_per_share_cache_absolute_counter,
|
|
recent_per_share_cached_value_bits,
|
|
recent_per_share_subscore_raw_u32,
|
|
cached_share_price_raw_u32,
|
|
chairman_salary_baseline,
|
|
chairman_salary_current,
|
|
chairman_bonus_year,
|
|
chairman_bonus_amount,
|
|
founding_year,
|
|
last_bankruptcy_year,
|
|
last_dividend_year,
|
|
current_issue_calendar_word,
|
|
current_issue_calendar_word_2,
|
|
prior_issue_calendar_word,
|
|
prior_issue_calendar_word_2,
|
|
city_connection_latch,
|
|
linked_transit_latch,
|
|
stat_band_root_0cfb_candidates: stat_band_root_0cfb_candidates
|
|
.iter()
|
|
.map(runtime_company_stat_band_candidate_from_save)
|
|
.collect(),
|
|
stat_band_root_0d7f_candidates: stat_band_root_0d7f_candidates
|
|
.iter()
|
|
.map(runtime_company_stat_band_candidate_from_save)
|
|
.collect(),
|
|
stat_band_root_1c47_candidates: stat_band_root_1c47_candidates
|
|
.iter()
|
|
.map(runtime_company_stat_band_candidate_from_save)
|
|
.collect(),
|
|
year_stat_family_qword_bits,
|
|
special_stat_family_232a_qword_bits,
|
|
issue_opinion_terms_raw_i32,
|
|
direct_control_transfer_float_fields_raw_u32,
|
|
direct_control_transfer_int_fields_raw_u32,
|
|
}),
|
|
});
|
|
}
|
|
|
|
Some(SmpLoadedCompanyRoster {
|
|
source_kind: "save-company-direct-records".to_string(),
|
|
semantic_family: "scenario-save-company-direct-records".to_string(),
|
|
observed_entry_count,
|
|
selected_company_id: selection_probe.map(|probe| probe.selected_company_id),
|
|
entries,
|
|
})
|
|
}
|
|
|
|
fn runtime_company_stat_band_candidate_from_save(
|
|
candidate: &SmpSaveDwordCandidate,
|
|
) -> crate::RuntimeCompanyStatBandCandidate {
|
|
crate::RuntimeCompanyStatBandCandidate {
|
|
label: candidate.label.clone(),
|
|
relative_offset: candidate.relative_offset,
|
|
relative_offset_hex: candidate.relative_offset_hex.clone(),
|
|
raw_u32: candidate.raw_u32,
|
|
raw_u32_hex: candidate.raw_u32_hex.clone(),
|
|
value_i32: candidate.value_i32,
|
|
value_f32_text: format!("{:.6}", candidate.value_f32),
|
|
}
|
|
}
|
|
|
|
fn build_save_company_stat_band_candidates(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
root_offset: usize,
|
|
label_prefix: &str,
|
|
word_count: usize,
|
|
) -> Option<Vec<SmpSaveDwordCandidate>> {
|
|
(0..word_count)
|
|
.map(|index| {
|
|
let relative_offset = root_offset.checked_add(index.checked_mul(4)?)?;
|
|
let label = format!("{label_prefix}_word_{}", index + 1);
|
|
build_save_dword_candidate(bytes, record_offset, &label, relative_offset)
|
|
})
|
|
.collect::<Option<Vec<_>>>()
|
|
}
|
|
|
|
fn build_save_company_stat_qword_bits(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
root_offset: usize,
|
|
qword_count: usize,
|
|
) -> Option<Vec<u64>> {
|
|
(0..qword_count)
|
|
.map(|index| {
|
|
let relative_offset = root_offset.checked_add(index.checked_mul(8)?)?;
|
|
read_u64_at(bytes, record_offset + relative_offset)
|
|
})
|
|
.collect::<Option<Vec<_>>>()
|
|
}
|
|
|
|
fn build_save_i32_term_strip(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
root_offset: usize,
|
|
value_count: usize,
|
|
) -> Option<Vec<i32>> {
|
|
(0..value_count)
|
|
.map(|index| {
|
|
let relative_offset = root_offset.checked_add(index.checked_mul(4)?)?;
|
|
read_i32_at(bytes, record_offset + relative_offset)
|
|
})
|
|
.collect::<Option<Vec<_>>>()
|
|
}
|
|
|
|
fn build_save_u32_field_map(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
offsets: &[usize],
|
|
) -> Option<BTreeMap<u32, u32>> {
|
|
let mut fields = BTreeMap::new();
|
|
for relative_offset in offsets {
|
|
fields.insert(
|
|
u32::try_from(*relative_offset).ok()?,
|
|
read_u32_at(bytes, record_offset + *relative_offset)?,
|
|
);
|
|
}
|
|
Some(fields)
|
|
}
|
|
|
|
fn decode_save_company_current_year_stat_slot(
|
|
year_stat_family_qword_bits: &[u64],
|
|
slot_id: u32,
|
|
) -> Option<f64> {
|
|
let index = slot_id.checked_mul(crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN)? as usize;
|
|
let value = f64::from_bits(*year_stat_family_qword_bits.get(index)?);
|
|
value.is_finite().then_some(value)
|
|
}
|
|
|
|
fn detect_save_company_record_start_offset(
|
|
bytes: &[u8],
|
|
header_probe: &SmpSaveTaggedCollectionHeaderProbe,
|
|
) -> Option<usize> {
|
|
let observed_entry_count = header_probe.live_record_count as usize;
|
|
let record_stride = header_probe.direct_record_stride as usize;
|
|
let scan_limit = SAVE_COMPANY_RECORD_START_SCAN_LIMIT.min(record_stride);
|
|
let base_offset = header_probe.metadata_tag_offset.checked_add(4)?;
|
|
let mut best_start = None;
|
|
let mut best_score = 0usize;
|
|
|
|
for start in 0..scan_limit {
|
|
let mut score = 0usize;
|
|
let mut seen_ids = std::collections::BTreeSet::new();
|
|
let mut valid = true;
|
|
for index in 0..observed_entry_count {
|
|
let record_offset = match base_offset
|
|
.checked_add(start)
|
|
.and_then(|offset| offset.checked_add(index.checked_mul(record_stride)?))
|
|
{
|
|
Some(offset) => offset,
|
|
None => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
};
|
|
let company_id = match read_u32_at(bytes, record_offset) {
|
|
Some(value)
|
|
if value >= 1
|
|
&& value <= header_probe.live_id_bound
|
|
&& seen_ids.insert(value) =>
|
|
{
|
|
value
|
|
}
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
};
|
|
let active = match read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET)
|
|
{
|
|
Some(0 | 1) => {
|
|
score += 1;
|
|
true
|
|
}
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
};
|
|
let linked = match read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET,
|
|
) {
|
|
Some(value) if value <= 0x100 => value,
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
};
|
|
let name = match read_ascii_c_string_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_NAME_OFFSET,
|
|
SAVE_COMPANY_RECORD_NAME_MAX_LEN,
|
|
) {
|
|
Some(name) if !name.is_empty() && name.chars().all(is_save_name_char) => name,
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
};
|
|
score += name.len();
|
|
if active {
|
|
score += 8;
|
|
}
|
|
if linked != 0 {
|
|
score += 2;
|
|
}
|
|
if company_id == (index + 1) as u32 {
|
|
score += 4;
|
|
}
|
|
}
|
|
if valid && score > best_score {
|
|
best_score = score;
|
|
best_start = Some(start);
|
|
}
|
|
}
|
|
|
|
best_start
|
|
}
|
|
|
|
fn parse_save_company_total_debt(bytes: &[u8], record_offset: usize) -> Option<u64> {
|
|
let bond_count =
|
|
read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize;
|
|
let mut total = 0u64;
|
|
for slot_index in 0..bond_count {
|
|
let slot_offset = record_offset
|
|
.checked_add(SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET)?
|
|
.checked_add(slot_index.checked_mul(SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE)?)?;
|
|
let principal = read_i32_at(bytes, slot_offset)?;
|
|
if principal > 0 {
|
|
total = total.checked_add(principal as u64)?;
|
|
}
|
|
}
|
|
Some(total)
|
|
}
|
|
|
|
fn parse_save_company_live_bond_slots(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
) -> Option<Vec<crate::RuntimeCompanyBondSlot>> {
|
|
let bond_count =
|
|
read_u8_at(bytes, record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET)? as usize;
|
|
let mut slots = Vec::new();
|
|
for slot_index in 0..bond_count {
|
|
let slot_offset = record_offset
|
|
.checked_add(SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET)?
|
|
.checked_add(slot_index.checked_mul(SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE)?)?;
|
|
let principal = read_i32_at(bytes, slot_offset)?;
|
|
if principal <= 0 {
|
|
continue;
|
|
}
|
|
let maturity_year = read_u32_at(bytes, slot_offset + 4)?;
|
|
let coupon_rate_raw_u32 = read_u32_at(bytes, slot_offset + 8)?;
|
|
let coupon_rate = f32::from_bits(coupon_rate_raw_u32);
|
|
if !coupon_rate.is_finite() {
|
|
continue;
|
|
}
|
|
slots.push(crate::RuntimeCompanyBondSlot {
|
|
slot_index: slot_index as u32,
|
|
principal: principal as u32,
|
|
maturity_year,
|
|
coupon_rate_raw_u32,
|
|
});
|
|
}
|
|
Some(slots)
|
|
}
|
|
|
|
fn parse_save_company_largest_live_bond_principal(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
) -> Option<Option<u32>> {
|
|
let mut largest_live_principal: Option<u32> = None;
|
|
for slot in parse_save_company_live_bond_slots(bytes, record_offset)? {
|
|
largest_live_principal = Some(match largest_live_principal {
|
|
Some(current) => current.max(slot.principal),
|
|
None => slot.principal,
|
|
});
|
|
}
|
|
Some(largest_live_principal)
|
|
}
|
|
|
|
fn parse_save_company_highest_coupon_live_bond_principal(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
) -> Option<Option<u32>> {
|
|
let mut highest_coupon_principal = None;
|
|
let mut highest_coupon_rate = None;
|
|
for slot in parse_save_company_live_bond_slots(bytes, record_offset)? {
|
|
let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32);
|
|
match highest_coupon_rate {
|
|
Some(current_rate) if coupon_rate < current_rate => {}
|
|
Some(current_rate) if coupon_rate == current_rate => {
|
|
if let Some(current_principal) = highest_coupon_principal {
|
|
if slot.principal > current_principal {
|
|
highest_coupon_principal = Some(slot.principal);
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
highest_coupon_rate = Some(coupon_rate);
|
|
highest_coupon_principal = Some(slot.principal);
|
|
}
|
|
}
|
|
}
|
|
Some(highest_coupon_principal)
|
|
}
|
|
|
|
fn parse_save_company_available_track_laying_capacity(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
) -> Option<Option<u32>> {
|
|
let raw = read_i32_at(
|
|
bytes,
|
|
record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET,
|
|
)?;
|
|
if raw < 0 {
|
|
Some(None)
|
|
} else {
|
|
Some(Some(raw as u32))
|
|
}
|
|
}
|
|
|
|
fn build_save_dword_candidate(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
label: &str,
|
|
relative_offset: usize,
|
|
) -> Option<SmpSaveDwordCandidate> {
|
|
let raw_u32 = read_u32_at(bytes, record_offset + relative_offset)?;
|
|
Some(SmpSaveDwordCandidate {
|
|
label: label.to_string(),
|
|
relative_offset,
|
|
relative_offset_hex: format!("0x{relative_offset:x}"),
|
|
raw_u32,
|
|
raw_u32_hex: format!("0x{raw_u32:08x}"),
|
|
value_i32: raw_u32 as i32,
|
|
value_f32: f32::from_bits(raw_u32),
|
|
})
|
|
}
|
|
|
|
fn build_save_qword_candidate(
|
|
bytes: &[u8],
|
|
record_offset: usize,
|
|
relative_offset: usize,
|
|
) -> Option<SmpSaveScalarCandidate> {
|
|
let raw_u64 = read_u64_at(bytes, record_offset + relative_offset)?;
|
|
Some(SmpSaveScalarCandidate {
|
|
relative_offset,
|
|
relative_offset_hex: format!("0x{relative_offset:x}"),
|
|
raw_u64,
|
|
raw_u64_hex: format!("0x{raw_u64:016x}"),
|
|
value_i64: raw_u64 as i64,
|
|
value_f64: f64::from_bits(raw_u64),
|
|
})
|
|
}
|
|
|
|
fn derive_chairman_holdings_share_price_total(
|
|
holdings_by_company: &BTreeMap<u32, u32>,
|
|
company_share_prices: &BTreeMap<u32, i64>,
|
|
) -> Option<i64> {
|
|
let mut total = 0i64;
|
|
for (company_id, units) in holdings_by_company {
|
|
let share_price = *company_share_prices.get(company_id)?;
|
|
total = total.checked_add((*units as i64).checked_mul(share_price)?)?;
|
|
}
|
|
Some(total)
|
|
}
|
|
|
|
fn derive_chairman_cached_purchasing_power_total(
|
|
current_cash: i64,
|
|
cached_scalar_candidates: &[SmpSaveScalarCandidate],
|
|
) -> Option<i64> {
|
|
let strongest_cached_total = cached_scalar_candidates
|
|
.iter()
|
|
.filter_map(|candidate| round_f64_to_i64(candidate.value_f64))
|
|
.filter(|value| *value >= 0)
|
|
.max()?;
|
|
current_cash.checked_add(strongest_cached_total)
|
|
}
|
|
|
|
fn build_save_world_selection_role_analysis(
|
|
probe: &SmpSaveWorldSelectionContextProbe,
|
|
) -> SmpSaveWorldSelectionRoleAnalysis {
|
|
let chairman_slots = probe
|
|
.chairman_slot_selectors
|
|
.iter()
|
|
.copied()
|
|
.zip(probe.chairman_role_gate_bytes.iter().copied())
|
|
.enumerate()
|
|
.map(|(slot_index, (selector_byte, role_gate_byte))| {
|
|
SmpSaveWorldSelectionRoleAnalysisEntry {
|
|
slot_index,
|
|
selector_byte,
|
|
selector_byte_hex: format!("0x{selector_byte:02x}"),
|
|
role_gate_byte,
|
|
role_gate_byte_hex: format!("0x{role_gate_byte:02x}"),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
SmpSaveWorldSelectionRoleAnalysis {
|
|
selected_company_id: probe.selected_company_id,
|
|
selected_chairman_profile_id: probe.selected_chairman_profile_id,
|
|
campaign_override_flag: probe.campaign_override_flag,
|
|
campaign_override_flag_hex: probe.campaign_override_flag_hex.clone(),
|
|
chairman_slots,
|
|
}
|
|
}
|
|
|
|
fn parse_save_chairman_profile_table_probe(
|
|
bytes: &[u8],
|
|
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
selection_probe: Option<&SmpSaveWorldSelectionContextProbe>,
|
|
company_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
) -> Option<SmpLoadedChairmanProfileTable> {
|
|
let header_probe = header_probe?;
|
|
let observed_entry_count = header_probe.live_record_count as usize;
|
|
if observed_entry_count == 0 {
|
|
return Some(SmpLoadedChairmanProfileTable {
|
|
source_kind: "save-chairman-profile-direct-records".to_string(),
|
|
semantic_family: "scenario-save-chairman-profile-direct-records".to_string(),
|
|
observed_entry_count,
|
|
selected_chairman_profile_id: selection_probe
|
|
.map(|probe| probe.selected_chairman_profile_id),
|
|
entries: Vec::new(),
|
|
});
|
|
}
|
|
|
|
let record_start_offset =
|
|
detect_save_chairman_profile_record_start_offset(bytes, header_probe)?;
|
|
let record_stride = header_probe.direct_record_stride as usize;
|
|
let base_offset = header_probe
|
|
.metadata_tag_offset
|
|
.checked_add(4)?
|
|
.checked_add(record_start_offset)?;
|
|
let company_id_bound = company_header_probe
|
|
.map(|probe| probe.live_id_bound)
|
|
.unwrap_or(0);
|
|
|
|
let mut entries = Vec::with_capacity(observed_entry_count);
|
|
for index in 0..observed_entry_count {
|
|
let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?;
|
|
let profile_id = read_u32_at(bytes, record_offset)?;
|
|
let active = read_u32_at(bytes, record_offset + 4)? != 0;
|
|
let name = read_ascii_c_string_at(
|
|
bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET,
|
|
SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN,
|
|
)?;
|
|
let current_cash = round_f64_to_i64(read_f64_at(
|
|
bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET,
|
|
)?)?;
|
|
let linked_company_id = parse_nonzero_u32(
|
|
bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET,
|
|
)?;
|
|
let personality_byte_0x291 = read_u8_at(
|
|
bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET,
|
|
)?;
|
|
let cache_0 = round_f64_to_i64(read_f64_at(
|
|
bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET,
|
|
)?)?;
|
|
let cache_1 = round_f64_to_i64(read_f64_at(
|
|
bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET,
|
|
)?)?;
|
|
let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS
|
|
.iter()
|
|
.map(|relative_offset| {
|
|
build_save_qword_candidate(bytes, record_offset, *relative_offset)
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
let issue_opinion_terms_raw_i32 = build_save_i32_term_strip(
|
|
bytes,
|
|
record_offset,
|
|
SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET,
|
|
SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT,
|
|
)?;
|
|
let holdings_value_total = cache_0.max(cache_1).max(0);
|
|
let net_worth_total = current_cash.saturating_add(holdings_value_total);
|
|
let purchasing_power_total =
|
|
derive_chairman_cached_purchasing_power_total(current_cash, &cached_scalar_candidates)
|
|
.unwrap_or(net_worth_total);
|
|
let mut company_holdings = BTreeMap::new();
|
|
for company_id in 1..=company_id_bound {
|
|
let slot_offset = record_offset
|
|
.checked_add(SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET)?
|
|
.checked_add((company_id as usize).checked_mul(4)?)?;
|
|
let units = read_u32_at(bytes, slot_offset)?;
|
|
if units != 0 {
|
|
company_holdings.insert(company_id, units);
|
|
}
|
|
}
|
|
entries.push(SmpLoadedChairmanProfileEntry {
|
|
profile_id,
|
|
name,
|
|
active,
|
|
current_cash,
|
|
linked_company_id,
|
|
company_holdings,
|
|
holdings_value_total,
|
|
net_worth_total,
|
|
purchasing_power_total,
|
|
personality_byte_0x291: Some(personality_byte_0x291),
|
|
issue_opinion_terms_raw_i32,
|
|
});
|
|
}
|
|
|
|
Some(SmpLoadedChairmanProfileTable {
|
|
source_kind: "save-chairman-profile-direct-records".to_string(),
|
|
semantic_family: "scenario-save-chairman-profile-direct-records".to_string(),
|
|
observed_entry_count,
|
|
selected_chairman_profile_id: selection_probe
|
|
.map(|probe| probe.selected_chairman_profile_id),
|
|
entries,
|
|
})
|
|
}
|
|
|
|
fn detect_save_chairman_profile_record_start_offset(
|
|
bytes: &[u8],
|
|
header_probe: &SmpSaveTaggedCollectionHeaderProbe,
|
|
) -> Option<usize> {
|
|
let observed_entry_count = header_probe.live_record_count as usize;
|
|
let record_stride = header_probe.direct_record_stride as usize;
|
|
let scan_limit = SAVE_CHAIRMAN_RECORD_START_SCAN_LIMIT.min(record_stride);
|
|
let base_offset = header_probe.metadata_tag_offset.checked_add(4)?;
|
|
let mut best_start = None;
|
|
let mut best_score = 0usize;
|
|
|
|
for start in 0..scan_limit {
|
|
let mut score = 0usize;
|
|
let mut seen_ids = std::collections::BTreeSet::new();
|
|
let mut valid = true;
|
|
for index in 0..observed_entry_count {
|
|
let record_offset = match base_offset
|
|
.checked_add(start)
|
|
.and_then(|offset| offset.checked_add(index.checked_mul(record_stride)?))
|
|
{
|
|
Some(offset) => offset,
|
|
None => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
};
|
|
let profile_id = match read_u32_at(bytes, record_offset) {
|
|
Some(value)
|
|
if value >= 1
|
|
&& value <= header_probe.live_id_bound
|
|
&& seen_ids.insert(value) =>
|
|
{
|
|
value
|
|
}
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
};
|
|
match read_u32_at(bytes, record_offset + 4) {
|
|
Some(0 | 1) => score += 1,
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
let name = match read_ascii_c_string_at(
|
|
bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET,
|
|
SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN,
|
|
) {
|
|
Some(name) if !name.is_empty() && name.chars().all(is_save_name_char) => name,
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
};
|
|
match read_u32_at(
|
|
bytes,
|
|
record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET,
|
|
) {
|
|
Some(value) if value <= 0x100 => score += (value != 0) as usize,
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
match read_f64_at(bytes, record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET) {
|
|
Some(value) if value.is_finite() && value.abs() < 1.0e12 => {
|
|
score += name.len() + 4;
|
|
}
|
|
_ => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
if profile_id == (index + 1) as u32 {
|
|
score += 4;
|
|
}
|
|
}
|
|
if valid && score > best_score {
|
|
best_score = score;
|
|
best_start = Some(start);
|
|
}
|
|
}
|
|
|
|
best_start
|
|
}
|
|
|
|
fn is_save_name_char(ch: char) -> bool {
|
|
ch.is_ascii_alphanumeric()
|
|
|| matches!(
|
|
ch,
|
|
' ' | '&' | '\'' | ',' | '.' | '-' | '/' | '(' | ')' | ':'
|
|
)
|
|
}
|
|
|
|
fn known_cargo_slot_definition(slot_id: u32) -> Option<KnownCargoSlotDefinition> {
|
|
KNOWN_CARGO_SLOT_DEFINITIONS
|
|
.iter()
|
|
.copied()
|
|
.find(|definition| definition.slot_id == slot_id)
|
|
}
|
|
|
|
fn known_cargo_slot_definition_for_descriptor_id(
|
|
descriptor_id: u32,
|
|
) -> Option<KnownCargoSlotDefinition> {
|
|
KNOWN_CARGO_SLOT_DEFINITIONS
|
|
.iter()
|
|
.copied()
|
|
.find(|definition| definition.descriptor_id == descriptor_id)
|
|
}
|
|
|
|
fn runtime_cargo_class_name(cargo_class: RuntimeCargoClass) -> &'static str {
|
|
match cargo_class {
|
|
RuntimeCargoClass::Factory => "factory",
|
|
RuntimeCargoClass::FarmMine => "farm_mine",
|
|
RuntimeCargoClass::Other => "other",
|
|
}
|
|
}
|
|
|
|
fn parse_event_runtime_collection_summary(
|
|
bytes: &[u8],
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
save_load_summary: Option<&SmpSaveLoadSummary>,
|
|
) -> Option<SmpLoadedEventRuntimeCollectionSummary> {
|
|
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);
|
|
|
|
for metadata_tag_offset in metadata_offsets {
|
|
let packed_state_version = read_u32_at(bytes, metadata_tag_offset + 2)?;
|
|
if packed_state_version != EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION {
|
|
continue;
|
|
}
|
|
|
|
let records_tag_offset = record_offsets
|
|
.iter()
|
|
.copied()
|
|
.find(|offset| *offset > metadata_tag_offset + 6)?;
|
|
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)?;
|
|
if metadata_payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN {
|
|
continue;
|
|
}
|
|
|
|
let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT)
|
|
.map(|index| read_u32_at(metadata_payload, index * 4))
|
|
.collect::<Option<Vec<_>>>()?;
|
|
let direct_collection_flag = header_words[0];
|
|
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 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 decoded_record_count = records
|
|
.iter()
|
|
.filter(|record| record.decode_status != "unsupported_framing")
|
|
.count();
|
|
let imported_runtime_record_count = records
|
|
.iter()
|
|
.filter(|record| record.executable_import_ready)
|
|
.count();
|
|
|
|
return Some(SmpLoadedEventRuntimeCollectionSummary {
|
|
source_kind: "packed-event-runtime-collection".to_string(),
|
|
mechanism_family: save_load_summary
|
|
.map(|summary| summary.mechanism_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string()),
|
|
mechanism_confidence: save_load_summary
|
|
.map(|summary| summary.mechanism_confidence.clone())
|
|
.unwrap_or_else(|| "inferred".to_string()),
|
|
container_profile_family: container_profile
|
|
.map(|profile| profile.profile_family.clone()),
|
|
metadata_tag_offset,
|
|
records_tag_offset,
|
|
close_tag_offset,
|
|
packed_state_version,
|
|
packed_state_version_hex: format!("0x{packed_state_version:08x}"),
|
|
live_id_bound,
|
|
live_record_count,
|
|
live_entry_ids,
|
|
decoded_record_count,
|
|
imported_runtime_record_count,
|
|
records,
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn decode_live_entry_ids_from_tombstone_bitset(
|
|
bitset: &[u8],
|
|
live_id_bound: u32,
|
|
) -> Option<Vec<u32>> {
|
|
let ids = decode_live_entry_ids_with_mapping(bitset, live_id_bound, false);
|
|
if ids.is_some() {
|
|
return ids;
|
|
}
|
|
decode_live_entry_ids_with_mapping(bitset, live_id_bound, true)
|
|
}
|
|
|
|
fn decode_live_entry_ids_with_mapping(
|
|
bitset: &[u8],
|
|
live_id_bound: u32,
|
|
subtract_one: bool,
|
|
) -> Option<Vec<u32>> {
|
|
let mut live_entry_ids = Vec::new();
|
|
|
|
for entry_id in 1..=live_id_bound {
|
|
let bit_index = if subtract_one {
|
|
entry_id.checked_sub(1)?
|
|
} else {
|
|
entry_id
|
|
};
|
|
let byte_index = usize::try_from(bit_index / 8).ok()?;
|
|
let bit_mask = 1u8.checked_shl(bit_index % 8).unwrap_or(0);
|
|
let tombstone_byte = *bitset.get(byte_index)?;
|
|
if tombstone_byte & bit_mask == 0 {
|
|
live_entry_ids.push(entry_id);
|
|
}
|
|
}
|
|
|
|
Some(live_entry_ids)
|
|
}
|
|
|
|
fn parse_event_runtime_record_summaries(
|
|
records_payload: &[u8],
|
|
records_payload_offset: usize,
|
|
live_entry_ids: &[u32],
|
|
) -> Vec<SmpLoadedPackedEventRecordSummary> {
|
|
try_parse_synthetic_event_runtime_record_summaries(
|
|
records_payload,
|
|
records_payload_offset,
|
|
live_entry_ids,
|
|
)
|
|
.or_else(|| {
|
|
try_parse_real_event_runtime_record_summaries(
|
|
records_payload,
|
|
records_payload_offset,
|
|
live_entry_ids,
|
|
)
|
|
})
|
|
.unwrap_or_else(|| {
|
|
build_unsupported_event_runtime_record_summaries(
|
|
live_entry_ids,
|
|
"0x4e9a payload did not match the current packed-event record decode harness",
|
|
)
|
|
})
|
|
}
|
|
|
|
fn try_parse_synthetic_event_runtime_record_summaries(
|
|
records_payload: &[u8],
|
|
records_payload_offset: usize,
|
|
live_entry_ids: &[u32],
|
|
) -> Option<Vec<SmpLoadedPackedEventRecordSummary>> {
|
|
if !records_payload.starts_with(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC) {
|
|
return None;
|
|
}
|
|
|
|
let mut cursor = PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC.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 record_len = usize::try_from(read_u32_at(records_payload, cursor)?).ok()?;
|
|
cursor += 4;
|
|
let record_body = records_payload.get(cursor..cursor + record_len)?;
|
|
records.push(parse_synthetic_event_runtime_record_summary(
|
|
record_body,
|
|
records_payload_offset + cursor,
|
|
record_index,
|
|
live_entry_id,
|
|
)?);
|
|
cursor += record_len;
|
|
}
|
|
|
|
if cursor != records_payload.len() {
|
|
return None;
|
|
}
|
|
|
|
Some(records)
|
|
}
|
|
|
|
fn parse_synthetic_event_runtime_record_summary(
|
|
record_body: &[u8],
|
|
payload_offset: usize,
|
|
record_index: usize,
|
|
live_entry_id: u32,
|
|
) -> Option<SmpLoadedPackedEventRecordSummary> {
|
|
if !record_body.starts_with(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC) {
|
|
return None;
|
|
}
|
|
|
|
let mut cursor = PACKED_EVENT_RECORD_SYNTHETIC_MAGIC.len();
|
|
let trigger_kind = read_u8_at(record_body, cursor)?;
|
|
cursor += 1;
|
|
let flags = read_u8_at(record_body, cursor)?;
|
|
cursor += 1;
|
|
let standalone_condition_row_count = usize::from(read_u8_at(record_body, cursor)?);
|
|
cursor += 1;
|
|
let action_count = usize::from(read_u8_at(record_body, cursor)?);
|
|
cursor += 1;
|
|
|
|
let mut grouped_effect_row_counts = Vec::with_capacity(4);
|
|
for _ in 0..4 {
|
|
grouped_effect_row_counts.push(usize::from(read_u8_at(record_body, cursor)?));
|
|
cursor += 1;
|
|
}
|
|
|
|
let mut text_bands = Vec::with_capacity(PACKED_EVENT_TEXT_BAND_LABELS.len());
|
|
for label in PACKED_EVENT_TEXT_BAND_LABELS {
|
|
let packed_len = usize::from(read_u16_at(record_body, cursor)?);
|
|
cursor += 2;
|
|
let band_bytes = record_body.get(cursor..cursor + packed_len)?;
|
|
cursor += packed_len;
|
|
text_bands.push(SmpLoadedPackedEventTextBandSummary {
|
|
label: label.to_string(),
|
|
packed_len,
|
|
present: packed_len != 0,
|
|
preview: ascii_preview(band_bytes),
|
|
});
|
|
}
|
|
|
|
let mut decoded_actions = Vec::with_capacity(action_count);
|
|
for _ in 0..action_count {
|
|
decoded_actions.push(parse_synthetic_packed_event_action(
|
|
record_body,
|
|
&mut cursor,
|
|
)?);
|
|
}
|
|
|
|
if cursor != record_body.len() {
|
|
return None;
|
|
}
|
|
|
|
let executable_import_ready = decoded_actions
|
|
.iter()
|
|
.all(runtime_effect_supported_for_save_import);
|
|
|
|
Some(SmpLoadedPackedEventRecordSummary {
|
|
record_index,
|
|
live_entry_id,
|
|
payload_offset: Some(payload_offset),
|
|
payload_len: Some(record_body.len()),
|
|
decode_status: if executable_import_ready {
|
|
"executable".to_string()
|
|
} else {
|
|
"parity_only".to_string()
|
|
},
|
|
payload_family: "synthetic_harness".to_string(),
|
|
trigger_kind: Some(trigger_kind),
|
|
active: Some(flags & 0x01 != 0),
|
|
marks_collection_dirty: Some(flags & 0x02 != 0),
|
|
one_shot: Some(flags & 0x04 != 0),
|
|
compact_control: None,
|
|
text_bands,
|
|
standalone_condition_row_count,
|
|
standalone_condition_rows: Vec::new(),
|
|
negative_sentinel_scope: None,
|
|
grouped_effect_row_counts,
|
|
grouped_effect_rows: Vec::new(),
|
|
decoded_conditions: Vec::new(),
|
|
decoded_actions,
|
|
executable_import_ready,
|
|
notes: vec!["decoded from the current synthetic packed-event record harness".to_string()],
|
|
})
|
|
}
|
|
|
|
fn try_parse_real_event_runtime_record_summaries(
|
|
records_payload: &[u8],
|
|
records_payload_offset: usize,
|
|
live_entry_ids: &[u32],
|
|
) -> Option<Vec<SmpLoadedPackedEventRecordSummary>> {
|
|
let mut cursor = 0usize;
|
|
let mut records = Vec::with_capacity(live_entry_ids.len());
|
|
|
|
for (record_index, live_entry_id) in live_entry_ids.iter().copied().enumerate() {
|
|
let (record, consumed_len) = parse_real_event_runtime_record_summary(
|
|
records_payload.get(cursor..)?,
|
|
records_payload_offset + cursor,
|
|
record_index,
|
|
live_entry_id,
|
|
)?;
|
|
records.push(record);
|
|
cursor += consumed_len;
|
|
}
|
|
|
|
if cursor != records_payload.len() {
|
|
return None;
|
|
}
|
|
|
|
Some(records)
|
|
}
|
|
|
|
fn parse_real_event_runtime_record_summary(
|
|
record_body: &[u8],
|
|
payload_offset: usize,
|
|
record_index: usize,
|
|
live_entry_id: u32,
|
|
) -> Option<(SmpLoadedPackedEventRecordSummary, usize)> {
|
|
let mut cursor = 0usize;
|
|
let mut text_bands = Vec::with_capacity(PACKED_EVENT_TEXT_BAND_LABELS.len());
|
|
for label in PACKED_EVENT_TEXT_BAND_LABELS {
|
|
let packed_len = usize::from(read_u16_at(record_body, cursor)?);
|
|
cursor += 2;
|
|
let band_bytes = record_body.get(cursor..cursor + packed_len)?;
|
|
cursor += packed_len;
|
|
text_bands.push(SmpLoadedPackedEventTextBandSummary {
|
|
label: label.to_string(),
|
|
packed_len,
|
|
present: packed_len != 0,
|
|
preview: ascii_preview(band_bytes),
|
|
});
|
|
}
|
|
|
|
let compact_control = parse_optional_real_compact_control_summary(record_body, &mut cursor)?;
|
|
|
|
if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER {
|
|
return None;
|
|
}
|
|
cursor += 2;
|
|
let standalone_condition_row_count = usize::from(read_u16_at(record_body, cursor)?);
|
|
cursor += 2;
|
|
|
|
let mut standalone_condition_rows = Vec::with_capacity(standalone_condition_row_count);
|
|
for row_index in 0..standalone_condition_row_count {
|
|
let row_bytes = record_body.get(cursor..cursor + PACKED_EVENT_REAL_CONDITION_ROW_LEN)?;
|
|
cursor += PACKED_EVENT_REAL_CONDITION_ROW_LEN;
|
|
let candidate_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?;
|
|
standalone_condition_rows.push(parse_real_condition_row_summary(
|
|
row_bytes,
|
|
row_index,
|
|
candidate_name,
|
|
)?);
|
|
}
|
|
|
|
if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER {
|
|
return None;
|
|
}
|
|
cursor += 2;
|
|
|
|
let mut grouped_effect_row_counts = Vec::with_capacity(4);
|
|
for _ in 0..4 {
|
|
grouped_effect_row_counts.push(usize::from(read_u16_at(record_body, cursor)?));
|
|
cursor += 2;
|
|
}
|
|
|
|
let mut grouped_effect_rows =
|
|
Vec::with_capacity(grouped_effect_row_counts.iter().sum::<usize>());
|
|
for (group_index, row_count) in grouped_effect_row_counts.iter().copied().enumerate() {
|
|
for row_index in 0..row_count {
|
|
let row_bytes =
|
|
record_body.get(cursor..cursor + PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN)?;
|
|
cursor += PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN;
|
|
let locomotive_name = parse_optional_u16_len_prefixed_string(record_body, &mut cursor)?;
|
|
grouped_effect_rows.push(parse_real_grouped_effect_row_summary(
|
|
row_bytes,
|
|
group_index,
|
|
row_index,
|
|
locomotive_name,
|
|
)?);
|
|
}
|
|
}
|
|
if let Some(control) = compact_control.as_ref() {
|
|
for row in &mut grouped_effect_rows {
|
|
let target_subject = derive_real_grouped_target_subject(row, control);
|
|
let target_scope_ordinal = control
|
|
.grouped_target_scope_ordinals_0x7fb
|
|
.get(row.group_index)
|
|
.copied();
|
|
row.grouped_target_subject = target_subject
|
|
.map(real_grouped_target_subject_name)
|
|
.map(str::to_string);
|
|
row.grouped_target_scope = derive_real_grouped_target_scope_name(
|
|
row,
|
|
control,
|
|
target_subject,
|
|
target_scope_ordinal,
|
|
);
|
|
let company_target_present = control
|
|
.grouped_target_scope_ordinals_0x7fb
|
|
.get(row.group_index)
|
|
.copied()
|
|
.and_then(real_grouped_company_target)
|
|
.is_some();
|
|
let chairman_target_present = control
|
|
.grouped_target_scope_ordinals_0x7fb
|
|
.get(row.group_index)
|
|
.copied()
|
|
.is_some_and(real_grouped_chairman_target_supported_in_runtime);
|
|
let territory_target_present = control
|
|
.grouped_territory_selectors_0x80f
|
|
.get(row.group_index)
|
|
.is_some_and(|selector| *selector >= 0);
|
|
if row.descriptor_id == 15
|
|
&& row.row_shape == "bool_toggle"
|
|
&& row.raw_scalar_value != 0
|
|
&& !company_target_present
|
|
&& !territory_target_present
|
|
{
|
|
row.notes
|
|
.push("retire train row is missing company and territory scope".to_string());
|
|
}
|
|
if row.descriptor_id == 3
|
|
&& row.row_shape == "bool_toggle"
|
|
&& row.raw_scalar_value != 0
|
|
&& (!company_target_present || !territory_target_present)
|
|
{
|
|
row.notes
|
|
.push("territory access row is missing company or territory scope".to_string());
|
|
}
|
|
if matches!(target_subject, Some(RealGroupedTargetSubject::Chairman))
|
|
&& !chairman_target_present
|
|
{
|
|
let ordinal = target_scope_ordinal.unwrap_or(u8::MAX);
|
|
row.notes.push(format!(
|
|
"chairman row uses unsupported grouped target scope ordinal {ordinal}"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let negative_sentinel_scope = compact_control.as_ref().and_then(|control| {
|
|
derive_negative_sentinel_scope_summary(&standalone_condition_rows, control)
|
|
});
|
|
let decoded_conditions =
|
|
decode_real_condition_rows(&standalone_condition_rows, negative_sentinel_scope.as_ref());
|
|
let decoded_actions = compact_control
|
|
.as_ref()
|
|
.map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control))
|
|
.unwrap_or_default();
|
|
let ordinary_condition_row_count = standalone_condition_rows
|
|
.iter()
|
|
.filter(|row| row.raw_condition_id >= 0)
|
|
.count();
|
|
let executable_import_ready = !grouped_effect_rows.is_empty()
|
|
&& decoded_actions.len() == grouped_effect_rows.len()
|
|
&& decoded_conditions.len() == ordinary_condition_row_count
|
|
&& decoded_actions
|
|
.iter()
|
|
.all(runtime_effect_supported_for_save_import)
|
|
&& decoded_conditions
|
|
.iter()
|
|
.all(runtime_condition_supported_for_save_import);
|
|
let consumed_len = cursor;
|
|
Some((
|
|
SmpLoadedPackedEventRecordSummary {
|
|
record_index,
|
|
live_entry_id,
|
|
payload_offset: Some(payload_offset),
|
|
payload_len: Some(consumed_len),
|
|
decode_status: "parity_only".to_string(),
|
|
payload_family: "real_packed_v1".to_string(),
|
|
trigger_kind: compact_control.as_ref().map(|control| control.mode_byte_0x7ef),
|
|
active: None,
|
|
marks_collection_dirty: None,
|
|
one_shot: compact_control
|
|
.as_ref()
|
|
.map(|control| control.one_shot_header_0x7f5 != 0),
|
|
compact_control,
|
|
text_bands,
|
|
standalone_condition_row_count,
|
|
standalone_condition_rows,
|
|
negative_sentinel_scope,
|
|
grouped_effect_row_counts,
|
|
grouped_effect_rows,
|
|
decoded_conditions,
|
|
decoded_actions,
|
|
executable_import_ready,
|
|
notes: vec![
|
|
"decoded from grounded real 0x4e9a row framing".to_string(),
|
|
"grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(),
|
|
],
|
|
},
|
|
consumed_len,
|
|
))
|
|
}
|
|
|
|
fn parse_optional_real_compact_control_summary(
|
|
record_body: &[u8],
|
|
cursor: &mut usize,
|
|
) -> Option<Option<SmpLoadedPackedEventCompactControlSummary>> {
|
|
if read_u16_at(record_body, *cursor)? == PACKED_EVENT_REAL_CONDITION_MARKER {
|
|
return Some(None);
|
|
}
|
|
|
|
let end = cursor.checked_add(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN)?;
|
|
let bytes = record_body.get(*cursor..end)?;
|
|
let mut local = 0usize;
|
|
let mode_byte_0x7ef = read_u8_at(bytes, local)?;
|
|
local += 1;
|
|
let primary_selector_0x7f0 = read_u32_at(bytes, local)?;
|
|
local += 4;
|
|
let grouped_mode_0x7f4 = read_u8_at(bytes, local)?;
|
|
local += 1;
|
|
let one_shot_header_0x7f5 = read_u32_at(bytes, local)?;
|
|
local += 4;
|
|
let modifier_flag_0x7f9 = read_u8_at(bytes, local)?;
|
|
local += 1;
|
|
let modifier_flag_0x7fa = read_u8_at(bytes, local)?;
|
|
local += 1;
|
|
|
|
let mut grouped_target_scope_ordinals_0x7fb = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
|
|
for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT {
|
|
grouped_target_scope_ordinals_0x7fb.push(read_u8_at(bytes, local)?);
|
|
local += 1;
|
|
}
|
|
|
|
let mut grouped_scope_checkboxes_0x7ff = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
|
|
for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT {
|
|
grouped_scope_checkboxes_0x7ff.push(read_u8_at(bytes, local)?);
|
|
local += 1;
|
|
}
|
|
|
|
let summary_toggle_0x800 = read_u8_at(bytes, local)?;
|
|
local += 1;
|
|
|
|
let mut grouped_territory_selectors_0x80f = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT);
|
|
for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT {
|
|
grouped_territory_selectors_0x80f.push(read_i32_at(bytes, local)?);
|
|
local += 4;
|
|
}
|
|
|
|
if local != bytes.len() {
|
|
return None;
|
|
}
|
|
if read_u16_at(record_body, end)? != PACKED_EVENT_REAL_CONDITION_MARKER {
|
|
return None;
|
|
}
|
|
|
|
*cursor = end;
|
|
Some(Some(SmpLoadedPackedEventCompactControlSummary {
|
|
mode_byte_0x7ef,
|
|
primary_selector_0x7f0,
|
|
grouped_mode_0x7f4,
|
|
one_shot_header_0x7f5,
|
|
modifier_flag_0x7f9,
|
|
modifier_flag_0x7fa,
|
|
grouped_target_scope_ordinals_0x7fb,
|
|
grouped_scope_checkboxes_0x7ff,
|
|
summary_toggle_0x800,
|
|
grouped_territory_selectors_0x80f,
|
|
}))
|
|
}
|
|
|
|
fn parse_real_condition_row_summary(
|
|
row_bytes: &[u8],
|
|
row_index: usize,
|
|
candidate_name: Option<String>,
|
|
) -> Option<SmpLoadedPackedEventConditionRowSummary> {
|
|
let raw_condition_id = read_u32_at(row_bytes, 0)? as i32;
|
|
let subtype = read_u8_at(row_bytes, 4)?;
|
|
let flag_bytes = row_bytes
|
|
.get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)?
|
|
.to_vec();
|
|
let candidate_name_display = candidate_name.clone();
|
|
let candidate_name_ref = candidate_name_display.as_deref();
|
|
let ordinary_metadata = real_ordinary_condition_metadata(raw_condition_id);
|
|
let comparator = ordinary_metadata
|
|
.and_then(|_| decode_real_condition_comparator(subtype))
|
|
.map(condition_comparator_label);
|
|
let metric = ordinary_metadata
|
|
.map(|metadata| real_ordinary_condition_metric_label(metadata, candidate_name_ref));
|
|
let threshold = ordinary_metadata.and_then(|_| decode_real_condition_threshold(&flag_bytes));
|
|
let requires_candidate_name_binding = ordinary_metadata.is_some_and(|metadata| {
|
|
matches!(
|
|
metadata.kind,
|
|
RealOrdinaryConditionKind::Numeric(
|
|
RealOrdinaryConditionMetric::Territory(_)
|
|
| RealOrdinaryConditionMetric::CompanyTerritory(_)
|
|
)
|
|
) && candidate_name.is_some()
|
|
});
|
|
let mut notes = Vec::new();
|
|
if raw_condition_id < 0 {
|
|
notes.push("negative sentinel-style condition row id".to_string());
|
|
}
|
|
if candidate_name.is_some() {
|
|
notes.push("condition row carries candidate-name side string".to_string());
|
|
}
|
|
if ordinary_metadata.is_none() && raw_condition_id >= 0 {
|
|
notes.push(
|
|
"ordinary condition id is not yet recovered in the checked-in condition table"
|
|
.to_string(),
|
|
);
|
|
}
|
|
if ordinary_metadata.is_some_and(|metadata| {
|
|
matches!(
|
|
metadata.kind,
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability)
|
|
) && candidate_name.is_none()
|
|
}) {
|
|
notes.push(
|
|
"candidate-availability condition row is missing its candidate-name side string"
|
|
.to_string(),
|
|
);
|
|
}
|
|
if ordinary_metadata.is_some_and(|metadata| {
|
|
matches!(
|
|
metadata.kind,
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::NamedLocomotiveAvailability
|
|
| RealWorldConditionKind::NamedLocomotiveCost
|
|
)
|
|
) && candidate_name.is_none()
|
|
}) {
|
|
notes.push("named locomotive condition row is missing its side-string binding".to_string());
|
|
}
|
|
if ordinary_metadata.is_some_and(|metadata| {
|
|
matches!(
|
|
metadata.kind,
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot)
|
|
) && candidate_name.is_none()
|
|
}) {
|
|
notes.push(
|
|
"named cargo-production condition row is missing its side-string binding".to_string(),
|
|
);
|
|
}
|
|
if ordinary_metadata.is_some_and(|metadata| {
|
|
matches!(
|
|
metadata.kind,
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::FactoryProductionTotal
|
|
| RealWorldConditionKind::FarmMineProductionTotal
|
|
| RealWorldConditionKind::OtherCargoProductionTotal
|
|
)
|
|
)
|
|
}) {
|
|
notes.push(
|
|
"checked-in RT3.lng label is known, but this cargo aggregate condition family is not yet lowered"
|
|
.to_string(),
|
|
);
|
|
}
|
|
if ordinary_metadata.is_some_and(|metadata| {
|
|
matches!(
|
|
metadata.kind,
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot)
|
|
) && candidate_name_ref
|
|
.and_then(recovered_cargo_production_slot_from_condition_name)
|
|
.is_none()
|
|
}) {
|
|
notes.push(
|
|
"named cargo-production condition side string does not yet map to a checked-in cargo slot"
|
|
.to_string(),
|
|
);
|
|
}
|
|
Some(SmpLoadedPackedEventConditionRowSummary {
|
|
row_index,
|
|
raw_condition_id,
|
|
subtype,
|
|
flag_bytes,
|
|
candidate_name,
|
|
comparator,
|
|
metric,
|
|
semantic_family: ordinary_metadata
|
|
.map(|metadata| real_ordinary_condition_semantic_family(metadata).to_string()),
|
|
semantic_preview: ordinary_metadata.and_then(|metadata| {
|
|
threshold.map(|value| {
|
|
let comparator_text = decode_real_condition_comparator(subtype)
|
|
.map(condition_comparator_symbol)
|
|
.unwrap_or("?");
|
|
let metric_label =
|
|
real_ordinary_condition_metric_label(metadata, candidate_name_ref);
|
|
format!("Test {} {} {}", metric_label, comparator_text, value)
|
|
})
|
|
}),
|
|
recovered_cargo_slot: ordinary_metadata.and_then(|metadata| match metadata.kind {
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => {
|
|
candidate_name_ref.and_then(recovered_cargo_production_slot_from_condition_name)
|
|
}
|
|
_ => None,
|
|
}),
|
|
recovered_cargo_class: ordinary_metadata.and_then(|metadata| match metadata.kind {
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => {
|
|
candidate_name_ref
|
|
.and_then(recovered_cargo_production_slot_from_condition_name)
|
|
.and_then(known_cargo_slot_definition)
|
|
.map(|definition| runtime_cargo_class_name(definition.cargo_class).to_string())
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::FactoryProductionTotal,
|
|
) => Some("factory".to_string()),
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::FarmMineProductionTotal,
|
|
) => Some("farm_mine".to_string()),
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::OtherCargoProductionTotal,
|
|
) => Some("other".to_string()),
|
|
_ => None,
|
|
}),
|
|
requires_candidate_name_binding,
|
|
notes,
|
|
})
|
|
}
|
|
|
|
fn derive_negative_sentinel_scope_summary(
|
|
rows: &[SmpLoadedPackedEventConditionRowSummary],
|
|
control: &SmpLoadedPackedEventCompactControlSummary,
|
|
) -> Option<SmpLoadedPackedEventNegativeSentinelScopeSummary> {
|
|
let source_row_indexes = rows
|
|
.iter()
|
|
.filter(|row| row.raw_condition_id == -1)
|
|
.map(|row| row.row_index)
|
|
.collect::<Vec<_>>();
|
|
if source_row_indexes.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some(SmpLoadedPackedEventNegativeSentinelScopeSummary {
|
|
company_test_scope: decode_company_condition_test_scope(control.modifier_flag_0x7f9)?,
|
|
player_test_scope: decode_player_condition_test_scope(control.modifier_flag_0x7fa)?,
|
|
territory_scope_selector_is_0x63: control.primary_selector_0x7f0 == 0x63,
|
|
source_row_indexes,
|
|
})
|
|
}
|
|
|
|
fn decode_company_condition_test_scope(value: u8) -> Option<RuntimeCompanyConditionTestScope> {
|
|
match value {
|
|
0 => Some(RuntimeCompanyConditionTestScope::Disabled),
|
|
1 => Some(RuntimeCompanyConditionTestScope::AllCompanies),
|
|
2 => Some(RuntimeCompanyConditionTestScope::SelectedCompanyOnly),
|
|
3 => Some(RuntimeCompanyConditionTestScope::AiCompaniesOnly),
|
|
4 => Some(RuntimeCompanyConditionTestScope::HumanCompaniesOnly),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn decode_player_condition_test_scope(value: u8) -> Option<RuntimePlayerConditionTestScope> {
|
|
match value {
|
|
0 => Some(RuntimePlayerConditionTestScope::Disabled),
|
|
1 => Some(RuntimePlayerConditionTestScope::AllPlayers),
|
|
2 => Some(RuntimePlayerConditionTestScope::SelectedPlayerOnly),
|
|
3 => Some(RuntimePlayerConditionTestScope::AiPlayersOnly),
|
|
4 => Some(RuntimePlayerConditionTestScope::HumanPlayersOnly),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn real_ordinary_condition_metadata(
|
|
raw_condition_id: i32,
|
|
) -> Option<RealOrdinaryConditionMetadata> {
|
|
REAL_ORDINARY_CONDITION_METADATA
|
|
.iter()
|
|
.copied()
|
|
.find(|metadata| metadata.raw_condition_id == raw_condition_id)
|
|
.or_else(|| {
|
|
known_special_condition_definition_for_label_id(raw_condition_id as u32).map(
|
|
|definition| {
|
|
let kind = if let Some(world_toggle) =
|
|
real_grouped_effect_descriptor_metadata(110 + definition.slot_index as u32)
|
|
.filter(|metadata| metadata.parameter_family == "world_flag_toggle")
|
|
{
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag {
|
|
key: world_toggle.runtime_key.unwrap_or(world_toggle.label),
|
|
})
|
|
} else {
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::SpecialCondition {
|
|
label: definition.label,
|
|
},
|
|
)
|
|
};
|
|
RealOrdinaryConditionMetadata {
|
|
raw_condition_id,
|
|
label: definition.label,
|
|
kind,
|
|
}
|
|
},
|
|
)
|
|
})
|
|
}
|
|
|
|
fn real_ordinary_condition_metric_label(
|
|
metadata: RealOrdinaryConditionMetadata,
|
|
candidate_name: Option<&str>,
|
|
) -> String {
|
|
match metadata.kind {
|
|
RealOrdinaryConditionKind::Numeric(_) => metadata.label.to_string(),
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition {
|
|
label,
|
|
}) => {
|
|
format!("Special Condition: {label}")
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => {
|
|
match candidate_name {
|
|
Some(name) => format!("Candidate Availability: {name}"),
|
|
None => "Candidate Availability".to_string(),
|
|
}
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::NamedLocomotiveAvailability,
|
|
) => match candidate_name {
|
|
Some(name) => format!("Named Locomotive Availability: {name}"),
|
|
None => "Named Locomotive Availability".to_string(),
|
|
},
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => {
|
|
match candidate_name {
|
|
Some(name) => format!("Named Locomotive Cost: {name}"),
|
|
None => "Named Locomotive Cost".to_string(),
|
|
}
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => {
|
|
match candidate_name {
|
|
Some(name) => format!("Cargo Production: {name}"),
|
|
None => "Cargo Production".to_string(),
|
|
}
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => {
|
|
"Cargo Production Total".to_string()
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal) => {
|
|
"Factory Production Total".to_string()
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FarmMineProductionTotal) => {
|
|
"Farm/Mine Production Total".to_string()
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::OtherCargoProductionTotal,
|
|
) => "Other Cargo Production Total".to_string(),
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::LimitedTrackBuildingAmount,
|
|
) => "Limited Track Building Amount".to_string(),
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => {
|
|
"Territory Access Cost".to_string()
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => {
|
|
"Economic Status".to_string()
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { .. }) => {
|
|
format!("World Flag: {}", metadata.label)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn real_ordinary_condition_semantic_family(
|
|
metadata: RealOrdinaryConditionMetadata,
|
|
) -> &'static str {
|
|
match metadata.kind {
|
|
RealOrdinaryConditionKind::Numeric(_) => "numeric_threshold",
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { .. }) => {
|
|
"world_flag_equals"
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::NamedLocomotiveAvailability
|
|
| RealWorldConditionKind::NamedLocomotiveCost
|
|
| RealWorldConditionKind::CargoProductionSlot
|
|
| RealWorldConditionKind::CargoProductionTotal
|
|
| RealWorldConditionKind::FactoryProductionTotal
|
|
| RealWorldConditionKind::FarmMineProductionTotal
|
|
| RealWorldConditionKind::OtherCargoProductionTotal
|
|
| RealWorldConditionKind::LimitedTrackBuildingAmount
|
|
| RealWorldConditionKind::TerritoryAccessCost,
|
|
) => "world_scalar_threshold",
|
|
RealOrdinaryConditionKind::WorldState(_) => "world_state_threshold",
|
|
}
|
|
}
|
|
|
|
fn decode_real_condition_comparator(subtype: u8) -> Option<RuntimeConditionComparator> {
|
|
match subtype {
|
|
0 => Some(RuntimeConditionComparator::Ge),
|
|
1 => Some(RuntimeConditionComparator::Le),
|
|
2 => Some(RuntimeConditionComparator::Gt),
|
|
3 => Some(RuntimeConditionComparator::Lt),
|
|
4 => Some(RuntimeConditionComparator::Eq),
|
|
5 => Some(RuntimeConditionComparator::Ne),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn decode_real_condition_threshold(flag_bytes: &[u8]) -> Option<i64> {
|
|
let raw = flag_bytes.get(0..4)?;
|
|
let mut bytes = [0u8; 4];
|
|
bytes.copy_from_slice(raw);
|
|
Some(i32::from_le_bytes(bytes).into())
|
|
}
|
|
|
|
fn condition_comparator_label(comparator: RuntimeConditionComparator) -> String {
|
|
match comparator {
|
|
RuntimeConditionComparator::Ge => "ge".to_string(),
|
|
RuntimeConditionComparator::Le => "le".to_string(),
|
|
RuntimeConditionComparator::Gt => "gt".to_string(),
|
|
RuntimeConditionComparator::Lt => "lt".to_string(),
|
|
RuntimeConditionComparator::Eq => "eq".to_string(),
|
|
RuntimeConditionComparator::Ne => "ne".to_string(),
|
|
}
|
|
}
|
|
|
|
fn condition_comparator_symbol(comparator: RuntimeConditionComparator) -> &'static str {
|
|
match comparator {
|
|
RuntimeConditionComparator::Ge => ">=",
|
|
RuntimeConditionComparator::Le => "<=",
|
|
RuntimeConditionComparator::Gt => ">",
|
|
RuntimeConditionComparator::Lt => "<",
|
|
RuntimeConditionComparator::Eq => "==",
|
|
RuntimeConditionComparator::Ne => "!=",
|
|
}
|
|
}
|
|
|
|
fn parse_real_grouped_effect_row_summary(
|
|
row_bytes: &[u8],
|
|
group_index: usize,
|
|
row_index: usize,
|
|
locomotive_name: Option<String>,
|
|
) -> Option<SmpLoadedPackedEventGroupedEffectRowSummary> {
|
|
let descriptor_id = read_u32_at(row_bytes, 0)?;
|
|
let raw_scalar_value = read_u32_at(row_bytes, 4)? as i32;
|
|
let opcode = read_u8_at(row_bytes, 8)?;
|
|
let value_byte_0x09 = read_u8_at(row_bytes, 9)?;
|
|
let value_dword_0x0d = read_u32_at(row_bytes, 0x0d)?;
|
|
let value_byte_0x11 = read_u8_at(row_bytes, 0x11)?;
|
|
let value_byte_0x12 = read_u8_at(row_bytes, 0x12)?;
|
|
let value_word_0x14 = read_u16_at(row_bytes, 0x14)?;
|
|
let value_word_0x16 = read_u16_at(row_bytes, 0x16)?;
|
|
let descriptor_metadata = real_grouped_effect_descriptor_metadata(descriptor_id);
|
|
let mut row_shape = classify_real_grouped_effect_row_shape(
|
|
opcode,
|
|
raw_scalar_value,
|
|
value_byte_0x11,
|
|
value_byte_0x12,
|
|
value_word_0x14,
|
|
value_word_0x16,
|
|
)
|
|
.to_string();
|
|
let mut semantic_family = classify_real_grouped_effect_semantic_family(
|
|
opcode,
|
|
raw_scalar_value,
|
|
value_byte_0x11,
|
|
value_byte_0x12,
|
|
value_word_0x14,
|
|
value_word_0x16,
|
|
)
|
|
.to_string();
|
|
if descriptor_metadata.is_some_and(|metadata| {
|
|
matches!(
|
|
metadata.parameter_family,
|
|
"special_condition_scalar" | "candidate_availability_scalar"
|
|
) && opcode == 3
|
|
&& value_byte_0x11 == 0
|
|
&& value_byte_0x12 == 0
|
|
&& value_word_0x14 == 0
|
|
&& value_word_0x16 == 0
|
|
}) {
|
|
row_shape = "scalar_assignment".to_string();
|
|
semantic_family = "scalar_assignment".to_string();
|
|
}
|
|
|
|
let mut notes = Vec::new();
|
|
if locomotive_name.is_some() {
|
|
notes.push("grouped effect row carries locomotive-name side string".to_string());
|
|
}
|
|
if let Some(metadata) = descriptor_metadata {
|
|
if metadata.runtime_status != RealGroupedEffectRuntimeStatus::Executable {
|
|
notes.push(format!(
|
|
"descriptor is recovered in the checked-in effect table as {} parity",
|
|
real_grouped_effect_runtime_status_name(metadata.runtime_status)
|
|
));
|
|
}
|
|
} else {
|
|
notes.push("descriptor id not yet recovered in the checked-in effect table".to_string());
|
|
}
|
|
if let Some(loco_id) = recovered_locomotive_availability_loco_id(descriptor_id) {
|
|
notes.push(format!(
|
|
"locomotive availability descriptor maps to live locomotive id {loco_id}"
|
|
));
|
|
}
|
|
if let Some(loco_id) = recovered_locomotive_cost_loco_id(descriptor_id) {
|
|
notes.push(format!(
|
|
"locomotive cost descriptor maps to live locomotive id {loco_id}"
|
|
));
|
|
}
|
|
if let Some(cargo_slot) = recovered_cargo_production_slot(descriptor_id) {
|
|
notes.push(format!(
|
|
"cargo-production descriptor maps to world production slot {cargo_slot}"
|
|
));
|
|
}
|
|
if let Some(cargo_label) = grounded_named_cargo_production_label(descriptor_id) {
|
|
notes.push(format!(
|
|
"named cargo production descriptor maps to cargo {cargo_label}"
|
|
));
|
|
}
|
|
if descriptor_metadata.is_some_and(|metadata| metadata.parameter_family == "cargo_price_scalar")
|
|
{
|
|
if let Some(cargo_label) = grounded_named_cargo_price_label(descriptor_id) {
|
|
notes.push(format!(
|
|
"named cargo price descriptor maps to cargo {cargo_label}"
|
|
));
|
|
}
|
|
}
|
|
|
|
Some(SmpLoadedPackedEventGroupedEffectRowSummary {
|
|
group_index,
|
|
row_index,
|
|
descriptor_id,
|
|
descriptor_label: descriptor_metadata.map(|metadata| metadata.label.to_string()),
|
|
target_mask_bits: descriptor_metadata.map(|metadata| metadata.target_mask_bits),
|
|
parameter_family: descriptor_metadata.map(|metadata| metadata.parameter_family.to_string()),
|
|
grouped_target_subject: None,
|
|
grouped_target_scope: None,
|
|
opcode,
|
|
raw_scalar_value,
|
|
value_byte_0x09,
|
|
value_dword_0x0d,
|
|
value_byte_0x11,
|
|
value_byte_0x12,
|
|
value_word_0x14,
|
|
value_word_0x16,
|
|
row_shape,
|
|
semantic_family: Some(semantic_family.clone()),
|
|
semantic_preview: Some(build_real_grouped_effect_semantic_preview(
|
|
descriptor_metadata.map(|metadata| metadata.label),
|
|
&semantic_family,
|
|
raw_scalar_value,
|
|
value_byte_0x11,
|
|
value_byte_0x12,
|
|
value_word_0x14,
|
|
value_word_0x16,
|
|
)),
|
|
recovered_cargo_slot: recovered_cargo_production_slot(descriptor_id),
|
|
recovered_cargo_class: recovered_cargo_production_slot(descriptor_id)
|
|
.and_then(known_cargo_slot_definition)
|
|
.map(|definition| runtime_cargo_class_name(definition.cargo_class).to_string()),
|
|
recovered_cargo_label: grounded_named_cargo_production_label(descriptor_id)
|
|
.or_else(|| {
|
|
descriptor_metadata
|
|
.filter(|metadata| metadata.parameter_family == "cargo_price_scalar")
|
|
.and_then(|_| grounded_named_cargo_price_label(descriptor_id))
|
|
})
|
|
.map(ToString::to_string),
|
|
recovered_locomotive_id: recovered_locomotive_availability_loco_id(descriptor_id)
|
|
.or_else(|| recovered_locomotive_cost_loco_id(descriptor_id)),
|
|
locomotive_name,
|
|
notes,
|
|
})
|
|
}
|
|
|
|
fn decode_real_condition_rows(
|
|
rows: &[SmpLoadedPackedEventConditionRowSummary],
|
|
negative_sentinel_scope: Option<&SmpLoadedPackedEventNegativeSentinelScopeSummary>,
|
|
) -> Vec<RuntimeCondition> {
|
|
rows.iter()
|
|
.filter(|row| row.raw_condition_id >= 0)
|
|
.filter_map(|row| decode_real_condition_row(row, negative_sentinel_scope))
|
|
.collect()
|
|
}
|
|
|
|
fn decode_real_condition_row(
|
|
row: &SmpLoadedPackedEventConditionRowSummary,
|
|
negative_sentinel_scope: Option<&SmpLoadedPackedEventNegativeSentinelScopeSummary>,
|
|
) -> Option<RuntimeCondition> {
|
|
let metadata = real_ordinary_condition_metadata(row.raw_condition_id)?;
|
|
let comparator = decode_real_condition_comparator(row.subtype)?;
|
|
let value = decode_real_condition_threshold(&row.flag_bytes)?;
|
|
match metadata.kind {
|
|
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::WorldVariable(index)) => {
|
|
Some(RuntimeCondition::WorldVariableThreshold {
|
|
index,
|
|
comparator,
|
|
value,
|
|
})
|
|
}
|
|
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(metric)) => {
|
|
Some(RuntimeCondition::CompanyNumericThreshold {
|
|
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
|
metric,
|
|
comparator,
|
|
value,
|
|
})
|
|
}
|
|
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyVariable(index)) => {
|
|
Some(RuntimeCondition::CompanyVariableThreshold {
|
|
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
|
index,
|
|
comparator,
|
|
value,
|
|
})
|
|
}
|
|
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::PlayerVariable(index)) => {
|
|
negative_sentinel_scope.and_then(|scope| {
|
|
real_condition_player_target(scope).map(|target| {
|
|
RuntimeCondition::PlayerVariableThreshold {
|
|
target,
|
|
index,
|
|
comparator,
|
|
value,
|
|
}
|
|
})
|
|
})
|
|
}
|
|
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(metric)) => {
|
|
negative_sentinel_scope.and_then(|scope| {
|
|
real_condition_chairman_target(scope).map(|target| {
|
|
RuntimeCondition::ChairmanNumericThreshold {
|
|
target,
|
|
metric,
|
|
comparator,
|
|
value,
|
|
}
|
|
})
|
|
})
|
|
}
|
|
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::TerritoryVariable(
|
|
index,
|
|
)) => negative_sentinel_scope
|
|
.filter(|scope| scope.territory_scope_selector_is_0x63)
|
|
.map(|_| RuntimeCondition::TerritoryVariableThreshold {
|
|
target: RuntimeTerritoryTarget::AllTerritories,
|
|
index,
|
|
comparator,
|
|
value,
|
|
}),
|
|
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => {
|
|
negative_sentinel_scope
|
|
.filter(|scope| scope.territory_scope_selector_is_0x63)
|
|
.map(|_| RuntimeCondition::TerritoryNumericThreshold {
|
|
target: RuntimeTerritoryTarget::AllTerritories,
|
|
metric,
|
|
comparator,
|
|
value,
|
|
})
|
|
}
|
|
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
|
|
metric,
|
|
)) => negative_sentinel_scope
|
|
.filter(|scope| scope.territory_scope_selector_is_0x63)
|
|
.map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold {
|
|
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
|
territory: RuntimeTerritoryTarget::AllTerritories,
|
|
metric,
|
|
comparator,
|
|
value,
|
|
}),
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition {
|
|
label,
|
|
}) => Some(RuntimeCondition::SpecialConditionThreshold {
|
|
label: label.to_string(),
|
|
comparator,
|
|
value,
|
|
}),
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => row
|
|
.candidate_name
|
|
.as_ref()
|
|
.map(|name| RuntimeCondition::CandidateAvailabilityThreshold {
|
|
name: name.clone(),
|
|
comparator,
|
|
value,
|
|
}),
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => {
|
|
row.candidate_name.as_ref().and_then(|name| {
|
|
recovered_cargo_production_slot_from_condition_name(name).map(|slot| {
|
|
let label = known_cargo_slot_definition(slot)
|
|
.map(|definition| definition.label.to_string())
|
|
.unwrap_or_else(|| name.clone());
|
|
RuntimeCondition::CargoProductionSlotThreshold {
|
|
slot,
|
|
label,
|
|
comparator,
|
|
value,
|
|
}
|
|
})
|
|
})
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::NamedLocomotiveAvailability,
|
|
) => row.candidate_name.as_ref().map(|name| {
|
|
RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
|
|
name: name.clone(),
|
|
comparator,
|
|
value,
|
|
}
|
|
}),
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => row
|
|
.candidate_name
|
|
.as_ref()
|
|
.map(|name| RuntimeCondition::NamedLocomotiveCostThreshold {
|
|
name: name.clone(),
|
|
comparator,
|
|
value,
|
|
}),
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => {
|
|
Some(RuntimeCondition::CargoProductionTotalThreshold { comparator, value })
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal) => {
|
|
Some(RuntimeCondition::FactoryProductionTotalThreshold { comparator, value })
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FarmMineProductionTotal) => {
|
|
Some(RuntimeCondition::FarmMineProductionTotalThreshold { comparator, value })
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::OtherCargoProductionTotal,
|
|
) => Some(RuntimeCondition::OtherCargoProductionTotalThreshold { comparator, value }),
|
|
RealOrdinaryConditionKind::WorldState(
|
|
RealWorldConditionKind::LimitedTrackBuildingAmount,
|
|
) => Some(RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value }),
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => {
|
|
Some(RuntimeCondition::TerritoryAccessCostThreshold { comparator, value })
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => {
|
|
Some(RuntimeCondition::EconomicStatusCodeThreshold { comparator, value })
|
|
}
|
|
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { key }) => {
|
|
decode_world_flag_condition(comparator, value, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn decode_world_flag_condition(
|
|
comparator: RuntimeConditionComparator,
|
|
value: i64,
|
|
key: &'static str,
|
|
) -> Option<RuntimeCondition> {
|
|
let bool_value = match (comparator, value) {
|
|
(RuntimeConditionComparator::Eq, 0) | (RuntimeConditionComparator::Ne, 1) => false,
|
|
(RuntimeConditionComparator::Eq, 1) | (RuntimeConditionComparator::Ne, 0) => true,
|
|
_ => return None,
|
|
};
|
|
Some(RuntimeCondition::WorldFlagEquals {
|
|
key: key.to_string(),
|
|
value: bool_value,
|
|
})
|
|
}
|
|
|
|
fn real_condition_chairman_target(
|
|
scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary,
|
|
) -> Option<RuntimeChairmanTarget> {
|
|
match scope.player_test_scope {
|
|
RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimeChairmanTarget::AllActive),
|
|
RuntimePlayerConditionTestScope::SelectedPlayerOnly => {
|
|
Some(RuntimeChairmanTarget::SelectedChairman)
|
|
}
|
|
RuntimePlayerConditionTestScope::AiPlayersOnly => Some(RuntimeChairmanTarget::AiChairmen),
|
|
RuntimePlayerConditionTestScope::HumanPlayersOnly => {
|
|
Some(RuntimeChairmanTarget::HumanChairmen)
|
|
}
|
|
RuntimePlayerConditionTestScope::Disabled => None,
|
|
}
|
|
}
|
|
|
|
fn real_condition_player_target(
|
|
scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary,
|
|
) -> Option<RuntimePlayerTarget> {
|
|
match scope.player_test_scope {
|
|
RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimePlayerTarget::AllActive),
|
|
RuntimePlayerConditionTestScope::SelectedPlayerOnly => {
|
|
Some(RuntimePlayerTarget::SelectedPlayer)
|
|
}
|
|
RuntimePlayerConditionTestScope::AiPlayersOnly => Some(RuntimePlayerTarget::AiPlayers),
|
|
RuntimePlayerConditionTestScope::HumanPlayersOnly => {
|
|
Some(RuntimePlayerTarget::HumanPlayers)
|
|
}
|
|
RuntimePlayerConditionTestScope::Disabled => None,
|
|
}
|
|
}
|
|
|
|
fn real_grouped_effect_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
recovered_cargo_price_descriptor_metadata(descriptor_id)
|
|
.or_else(|| recovered_cargo_economics_descriptor_metadata(descriptor_id))
|
|
.or_else(|| recovered_cargo_production_descriptor_metadata(descriptor_id))
|
|
.or_else(|| recovered_locomotive_availability_descriptor_metadata(descriptor_id))
|
|
.or_else(|| recovered_locomotive_cost_descriptor_metadata(descriptor_id))
|
|
.or_else(|| recovered_territory_access_cost_descriptor_metadata(descriptor_id))
|
|
.or_else(|| recovered_locomotive_policy_descriptor_metadata(descriptor_id))
|
|
.or_else(|| special_condition_world_scalar_descriptor_metadata(descriptor_id))
|
|
.or_else(|| special_condition_world_toggle_descriptor_metadata(descriptor_id))
|
|
.or_else(|| {
|
|
REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA
|
|
.iter()
|
|
.copied()
|
|
.find(|metadata| metadata.descriptor_id == descriptor_id)
|
|
})
|
|
.or_else(|| {
|
|
checked_in_event_effect_descriptor_rows()
|
|
.get(&descriptor_id)
|
|
.copied()
|
|
})
|
|
}
|
|
|
|
fn recovered_cargo_price_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
(descriptor_id == 105).then_some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: "All Cargo Prices",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "cargo_price_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
})
|
|
}
|
|
|
|
fn recovered_cargo_economics_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
match descriptor_id {
|
|
177 => Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: "All Cargo Production",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "cargo_production_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
}),
|
|
178 => Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: "All Factory Production",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "cargo_production_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
}),
|
|
179 => Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: "All Farm/Mine Production",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "cargo_production_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
const GROUNDED_NAMED_CARGO_PRODUCTION_LABELS: [(&str, &str); 50] = [
|
|
("Alcohol", "Alcohol Production"),
|
|
("Aluminum", "Aluminum Production"),
|
|
("Ammunition", "Ammunition Production"),
|
|
("Automobiles", "Automobiles Production"),
|
|
("Bauxite", "Bauxite Production"),
|
|
("Ceramics", "Ceramics Production"),
|
|
("Cheese", "Cheese Production"),
|
|
("Chemicals", "Chemicals Production"),
|
|
("Clothing", "Clothing Production"),
|
|
("Coal", "Coal Production"),
|
|
("Coffee", "Coffee Production"),
|
|
("Concrete", "Concrete Production"),
|
|
("Corn", "Corn Production"),
|
|
("Cotton", "Cotton Production"),
|
|
("Crystals", "Crystals Production"),
|
|
("Diesel", "Diesel Production"),
|
|
("Dye", "Dye Production"),
|
|
("Electronics", "Electronics Production"),
|
|
("Fertilizer", "Fertilizer Production"),
|
|
("Furniture", "Furniture Production"),
|
|
("Goods", "Goods Production"),
|
|
("Grain", "Grain Production"),
|
|
("Ingots", "Ingots Production"),
|
|
("Iron", "Iron Production"),
|
|
("Livestock", "Livestock Production"),
|
|
("Logs", "Logs Production"),
|
|
("Lumber", "Lumber Production"),
|
|
("Machinery", "Machinery Production"),
|
|
("Mail", "Mail Production"),
|
|
("Meat", "Meat Production"),
|
|
("Medicine", "Medicine Production"),
|
|
("Milk", "Milk Production"),
|
|
("Oil", "Oil Production"),
|
|
("Ore", "Ore Production"),
|
|
("Paper", "Paper Production"),
|
|
("Passengers", "Passengers Production"),
|
|
("Plastic", "Plastic Production"),
|
|
("Produce", "Produce Production"),
|
|
("Pulpwood", "Pulpwood Production"),
|
|
("Rice", "Rice Production"),
|
|
("Rubber", "Rubber Production"),
|
|
("Steel", "Steel Production"),
|
|
("Sugar", "Sugar Production"),
|
|
("Tires", "Tires Production"),
|
|
("Toys", "Toys Production"),
|
|
("Troops", "Troops Production"),
|
|
("Uranium", "Uranium Production"),
|
|
("Waste", "Waste Production"),
|
|
("Weapons", "Weapons Production"),
|
|
("Wool", "Wool Production"),
|
|
];
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct CheckedInCargoBindingsArtifact {
|
|
bindings: Vec<CheckedInCargoBindingRow>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct CheckedInCargoBindingRow {
|
|
descriptor_id: u32,
|
|
band: String,
|
|
cargo_name: String,
|
|
}
|
|
|
|
fn grounded_named_cargo_price_bindings() -> &'static BTreeMap<u32, (&'static str, &'static str)> {
|
|
static BINDINGS: OnceLock<BTreeMap<u32, (&'static str, &'static str)>> = OnceLock::new();
|
|
BINDINGS.get_or_init(|| {
|
|
let artifact: CheckedInCargoBindingsArtifact = serde_json::from_str(include_str!(
|
|
"../../../artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json"
|
|
))
|
|
.expect("checked-in cargo bindings artifact should parse");
|
|
artifact
|
|
.bindings
|
|
.into_iter()
|
|
.filter(|binding| binding.band == "cargo_price_named")
|
|
.map(|binding| {
|
|
let cargo_name = Box::leak(binding.cargo_name.into_boxed_str()) as &'static str;
|
|
let descriptor_label =
|
|
Box::leak(format!("{cargo_name} Price").into_boxed_str()) as &'static str;
|
|
(binding.descriptor_id, (cargo_name, descriptor_label))
|
|
})
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
pub(crate) fn grounded_named_cargo_price_label(descriptor_id: u32) -> Option<&'static str> {
|
|
grounded_named_cargo_price_bindings()
|
|
.get(&descriptor_id)
|
|
.map(|(cargo_label, _)| *cargo_label)
|
|
}
|
|
|
|
fn grounded_named_cargo_production_label(descriptor_id: u32) -> Option<&'static str> {
|
|
let index = descriptor_id.checked_sub(180)? as usize;
|
|
GROUNDED_NAMED_CARGO_PRODUCTION_LABELS
|
|
.get(index)
|
|
.map(|(cargo_label, _)| *cargo_label)
|
|
}
|
|
|
|
fn grounded_named_cargo_production_descriptor_label(descriptor_id: u32) -> Option<&'static str> {
|
|
let index = descriptor_id.checked_sub(180)? as usize;
|
|
GROUNDED_NAMED_CARGO_PRODUCTION_LABELS
|
|
.get(index)
|
|
.map(|(_, descriptor_label)| *descriptor_label)
|
|
}
|
|
|
|
fn recovered_cargo_production_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
if let Some(label) = grounded_named_cargo_production_descriptor_label(descriptor_id) {
|
|
return Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label,
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "cargo_production_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
});
|
|
}
|
|
recovered_cargo_production_label(descriptor_id).map(|label| {
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label,
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "cargo_production_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
}
|
|
})
|
|
}
|
|
|
|
fn recovered_cargo_production_slot(descriptor_id: u32) -> Option<u32> {
|
|
let slot = descriptor_id.checked_sub(229)?;
|
|
(1..=11).contains(&slot).then_some(slot)
|
|
}
|
|
|
|
fn recovered_locomotive_availability_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
if let Some(loco_id) = recovered_locomotive_availability_loco_id(descriptor_id) {
|
|
let label = recovered_locomotive_availability_label(loco_id);
|
|
let executable_in_runtime = (loco_id as usize) <= GROUNDED_LOCOMOTIVE_PREFIX.len();
|
|
return Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label,
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "locomotive_availability_scalar",
|
|
runtime_key: None,
|
|
runtime_status: if executable_in_runtime {
|
|
RealGroupedEffectRuntimeStatus::Executable
|
|
} else {
|
|
RealGroupedEffectRuntimeStatus::EvidenceBlocked
|
|
},
|
|
executable_in_runtime,
|
|
});
|
|
}
|
|
(457..=474)
|
|
.contains(&descriptor_id)
|
|
.then(|| RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: upper_band_locomotive_availability_label(descriptor_id),
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "locomotive_availability_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::EvidenceBlocked,
|
|
executable_in_runtime: false,
|
|
})
|
|
}
|
|
|
|
fn recovered_locomotive_availability_loco_id(descriptor_id: u32) -> Option<u32> {
|
|
if (241..=351).contains(&descriptor_id) {
|
|
return Some(descriptor_id - 240);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn grounded_locomotive_name(loco_id: u32) -> Option<&'static str> {
|
|
let index = loco_id.checked_sub(1)? as usize;
|
|
GROUNDED_LOCOMOTIVE_PREFIX.get(index).copied()
|
|
}
|
|
|
|
fn recovered_locomotive_availability_label(loco_id: u32) -> &'static str {
|
|
static LABELS: OnceLock<BTreeMap<u32, &'static str>> = OnceLock::new();
|
|
LABELS
|
|
.get_or_init(|| {
|
|
(1..=111)
|
|
.map(|loco_id| {
|
|
let label = grounded_locomotive_name(loco_id)
|
|
.map(|name| format!("{name} Availability"))
|
|
.unwrap_or_else(|| {
|
|
format!("Lower-Band Locomotive Availability Slot {loco_id}")
|
|
});
|
|
(loco_id, Box::leak(label.into_boxed_str()) as &'static str)
|
|
})
|
|
.collect()
|
|
})
|
|
.get(&loco_id)
|
|
.copied()
|
|
.expect("lower-band locomotive availability label should exist")
|
|
}
|
|
|
|
fn upper_band_locomotive_availability_label(descriptor_id: u32) -> &'static str {
|
|
static LABELS: OnceLock<BTreeMap<u32, &'static str>> = OnceLock::new();
|
|
LABELS
|
|
.get_or_init(|| {
|
|
(457..=474)
|
|
.map(|descriptor_id| {
|
|
let label = format!(
|
|
"Upper-Band Locomotive Availability Slot {}",
|
|
descriptor_id - 456
|
|
);
|
|
(
|
|
descriptor_id,
|
|
Box::leak(label.into_boxed_str()) as &'static str,
|
|
)
|
|
})
|
|
.collect()
|
|
})
|
|
.get(&descriptor_id)
|
|
.copied()
|
|
.expect("upper-band locomotive availability label should exist")
|
|
}
|
|
|
|
fn recovered_cargo_production_label(descriptor_id: u32) -> Option<&'static str> {
|
|
known_cargo_slot_definition_for_descriptor_id(descriptor_id).map(|definition| definition.label)
|
|
}
|
|
|
|
fn recovered_cargo_production_slot_from_condition_name(name: &str) -> Option<u32> {
|
|
KNOWN_CARGO_SLOT_DEFINITIONS
|
|
.iter()
|
|
.find(|definition| definition.label == name)
|
|
.map(|definition| definition.slot_id)
|
|
}
|
|
|
|
fn recovered_locomotive_cost_loco_id(descriptor_id: u32) -> Option<u32> {
|
|
if (352..=451).contains(&descriptor_id) {
|
|
return Some(descriptor_id - 351);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn recovered_locomotive_cost_label(descriptor_id: u32) -> Option<&'static str> {
|
|
static LABELS: OnceLock<BTreeMap<u32, &'static str>> = OnceLock::new();
|
|
LABELS
|
|
.get_or_init(|| {
|
|
(352..=451)
|
|
.filter_map(|descriptor_id| {
|
|
recovered_locomotive_cost_loco_id(descriptor_id).map(|loco_id| {
|
|
let label = grounded_locomotive_name(loco_id)
|
|
.map(|name| format!("{name} Cost"))
|
|
.unwrap_or_else(|| {
|
|
format!("Lower-Band Locomotive Cost Slot {loco_id}")
|
|
});
|
|
let label = Box::leak(label.into_boxed_str()) as &'static str;
|
|
(descriptor_id, label)
|
|
})
|
|
})
|
|
.collect()
|
|
})
|
|
.get(&descriptor_id)
|
|
.copied()
|
|
.or_else(|| upper_band_locomotive_cost_label(descriptor_id))
|
|
}
|
|
|
|
fn upper_band_locomotive_cost_label(descriptor_id: u32) -> Option<&'static str> {
|
|
static LABELS: OnceLock<BTreeMap<u32, &'static str>> = OnceLock::new();
|
|
LABELS
|
|
.get_or_init(|| {
|
|
(475..=502)
|
|
.map(|descriptor_id| {
|
|
let label = format!("Upper-Band Locomotive Cost Slot {}", descriptor_id - 474);
|
|
(
|
|
descriptor_id,
|
|
Box::leak(label.into_boxed_str()) as &'static str,
|
|
)
|
|
})
|
|
.collect()
|
|
})
|
|
.get(&descriptor_id)
|
|
.copied()
|
|
}
|
|
|
|
fn recovered_locomotive_cost_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
recovered_locomotive_cost_label(descriptor_id).map(|label| {
|
|
let executable_in_runtime = recovered_locomotive_cost_loco_id(descriptor_id)
|
|
.is_some_and(|loco_id| (loco_id as usize) <= GROUNDED_LOCOMOTIVE_PREFIX.len());
|
|
RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label,
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "locomotive_cost_scalar",
|
|
runtime_key: None,
|
|
runtime_status: if executable_in_runtime {
|
|
RealGroupedEffectRuntimeStatus::Executable
|
|
} else {
|
|
RealGroupedEffectRuntimeStatus::EvidenceBlocked
|
|
},
|
|
executable_in_runtime,
|
|
}
|
|
})
|
|
}
|
|
|
|
fn recovered_territory_access_cost_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
(descriptor_id == 453).then_some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: "Territory Access Cost",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "territory_access_cost_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
})
|
|
}
|
|
|
|
fn recovered_locomotive_policy_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
match descriptor_id {
|
|
454 => Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: "All Steam Locos Avail.",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "world_flag_toggle",
|
|
runtime_key: Some("world.all_steam_locos_available"),
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
}),
|
|
455 => Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: "All Diesel Locos Avail.",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "world_flag_toggle",
|
|
runtime_key: Some("world.all_diesel_locos_available"),
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
}),
|
|
456 => Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: "All Electric Locos Avail.",
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "world_flag_toggle",
|
|
runtime_key: Some("world.all_electric_locos_available"),
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn special_condition_world_scalar_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
let slot_index = descriptor_id.checked_sub(110)? as usize;
|
|
if slot_index != 12 {
|
|
return None;
|
|
}
|
|
let definition = KNOWN_SPECIAL_CONDITION_DEFINITIONS.get(slot_index)?;
|
|
if definition.hidden {
|
|
return None;
|
|
}
|
|
Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: definition.label,
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "world_track_build_limit_scalar",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
})
|
|
}
|
|
|
|
fn special_condition_world_toggle_descriptor_metadata(
|
|
descriptor_id: u32,
|
|
) -> Option<RealGroupedEffectDescriptorMetadata> {
|
|
let slot_index = descriptor_id.checked_sub(110)? as usize;
|
|
if !(1..=34).contains(&slot_index) || matches!(slot_index, 12 | 31) {
|
|
return None;
|
|
}
|
|
let definition = KNOWN_SPECIAL_CONDITION_DEFINITIONS.get(slot_index)?;
|
|
if definition.hidden {
|
|
return None;
|
|
}
|
|
Some(RealGroupedEffectDescriptorMetadata {
|
|
descriptor_id,
|
|
label: definition.label,
|
|
target_mask_bits: 0x08,
|
|
parameter_family: "world_flag_toggle",
|
|
runtime_key: None,
|
|
runtime_status: RealGroupedEffectRuntimeStatus::Executable,
|
|
executable_in_runtime: true,
|
|
})
|
|
}
|
|
|
|
fn classify_real_grouped_effect_semantic_family(
|
|
opcode: u8,
|
|
raw_scalar_value: i32,
|
|
value_byte_0x11: u8,
|
|
value_byte_0x12: u8,
|
|
value_word_0x14: u16,
|
|
value_word_0x16: u16,
|
|
) -> &'static str {
|
|
if opcode == 8 {
|
|
return "multivalue_scalar";
|
|
}
|
|
if value_byte_0x11 != 0 || value_byte_0x12 != 0 || value_word_0x14 != 0 || value_word_0x16 != 0
|
|
{
|
|
return "timed_duration";
|
|
}
|
|
if raw_scalar_value == 0 || raw_scalar_value == 1 {
|
|
return "bool_toggle";
|
|
}
|
|
"scalar_assignment"
|
|
}
|
|
|
|
fn classify_real_grouped_effect_row_shape(
|
|
opcode: u8,
|
|
raw_scalar_value: i32,
|
|
value_byte_0x11: u8,
|
|
value_byte_0x12: u8,
|
|
value_word_0x14: u16,
|
|
value_word_0x16: u16,
|
|
) -> &'static str {
|
|
if opcode == 8 {
|
|
return "multivalue_scalar";
|
|
}
|
|
if value_byte_0x11 != 0 || value_byte_0x12 != 0 || value_word_0x14 != 0 || value_word_0x16 != 0
|
|
{
|
|
return "timed_duration";
|
|
}
|
|
if raw_scalar_value == 0 || raw_scalar_value == 1 {
|
|
return "bool_toggle";
|
|
}
|
|
"scalar_assignment"
|
|
}
|
|
|
|
fn build_real_grouped_effect_semantic_preview(
|
|
descriptor_label: Option<&str>,
|
|
semantic_family: &str,
|
|
raw_scalar_value: i32,
|
|
value_byte_0x11: u8,
|
|
value_byte_0x12: u8,
|
|
value_word_0x14: u16,
|
|
value_word_0x16: u16,
|
|
) -> String {
|
|
let label = descriptor_label.unwrap_or("descriptor");
|
|
match semantic_family {
|
|
"bool_toggle" => {
|
|
let state = if raw_scalar_value == 0 {
|
|
"FALSE"
|
|
} else {
|
|
"TRUE"
|
|
};
|
|
format!("Set {label} to {state}")
|
|
}
|
|
"timed_duration" => format!(
|
|
"Set {label} to {raw_scalar_value} for {value_word_0x14} years {value_word_0x16} months"
|
|
),
|
|
"multivalue_scalar" => format!(
|
|
"Set {label} to {raw_scalar_value} with aux [{value_byte_0x11}, {value_byte_0x12}, {value_word_0x14}, {value_word_0x16}]"
|
|
),
|
|
_ => format!("Set {label} to {raw_scalar_value}"),
|
|
}
|
|
}
|
|
|
|
fn runtime_candidate_availability_name(label: &str) -> String {
|
|
label
|
|
.strip_suffix(" Availability")
|
|
.unwrap_or(label)
|
|
.to_string()
|
|
}
|
|
|
|
fn runtime_world_flag_key(
|
|
descriptor_metadata: RealGroupedEffectDescriptorMetadata,
|
|
) -> Option<String> {
|
|
descriptor_metadata
|
|
.runtime_key
|
|
.map(str::to_string)
|
|
.or_else(|| {
|
|
(descriptor_metadata.parameter_family == "world_flag_toggle")
|
|
.then(|| runtime_world_flag_key_from_label(descriptor_metadata.label))
|
|
})
|
|
}
|
|
|
|
pub(crate) fn runtime_world_flag_key_from_label(label: &str) -> String {
|
|
normalize_runtime_world_key(label)
|
|
}
|
|
|
|
fn runtime_world_scalar_key(
|
|
descriptor_metadata: RealGroupedEffectDescriptorMetadata,
|
|
) -> Option<String> {
|
|
descriptor_metadata
|
|
.runtime_key
|
|
.map(str::to_string)
|
|
.or_else(|| {
|
|
(descriptor_metadata.parameter_family == "world_scalar_override")
|
|
.then(|| normalize_runtime_world_key(descriptor_metadata.label))
|
|
})
|
|
}
|
|
|
|
pub(crate) fn runtime_world_scalar_key_from_label(label: &str) -> String {
|
|
normalize_runtime_world_key(label)
|
|
}
|
|
|
|
fn normalize_runtime_world_key(label: &str) -> String {
|
|
let mut key = String::with_capacity(label.len() + 6);
|
|
key.push_str("world.");
|
|
let mut last_was_underscore = false;
|
|
for ch in label.chars() {
|
|
if ch.is_ascii_alphanumeric() {
|
|
key.push(ch.to_ascii_lowercase());
|
|
last_was_underscore = false;
|
|
} else if !last_was_underscore {
|
|
key.push('_');
|
|
last_was_underscore = true;
|
|
}
|
|
}
|
|
while key.ends_with('_') {
|
|
key.pop();
|
|
}
|
|
key
|
|
}
|
|
|
|
fn real_grouped_company_governance_metric(
|
|
descriptor_metadata: RealGroupedEffectDescriptorMetadata,
|
|
) -> Option<RuntimeCompanyMetric> {
|
|
match descriptor_metadata.label {
|
|
"Credit Rating" => Some(RuntimeCompanyMetric::CreditRating),
|
|
"Prime Rate" => Some(RuntimeCompanyMetric::PrimeRate),
|
|
"Book Value Per Share" => Some(RuntimeCompanyMetric::BookValuePerShare),
|
|
"Investor Confidence" => Some(RuntimeCompanyMetric::InvestorConfidence),
|
|
"Management Attitude" => Some(RuntimeCompanyMetric::ManagementAttitude),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn derive_real_grouped_target_subject(
|
|
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
|
|
compact_control: &SmpLoadedPackedEventCompactControlSummary,
|
|
) -> Option<RealGroupedTargetSubject> {
|
|
if row.parameter_family.as_deref() == Some("company_governance_scalar") {
|
|
return Some(RealGroupedTargetSubject::Company);
|
|
}
|
|
if row.parameter_family.as_deref() == Some("world_scalar_override") {
|
|
return Some(RealGroupedTargetSubject::WholeGame);
|
|
}
|
|
match row.target_mask_bits {
|
|
Some(0x08) => Some(RealGroupedTargetSubject::WholeGame),
|
|
Some(0x01) => Some(RealGroupedTargetSubject::Company),
|
|
Some(0x04) => Some(RealGroupedTargetSubject::Territory),
|
|
Some(0x02) => match compact_control
|
|
.grouped_scope_checkboxes_0x7ff
|
|
.get(row.group_index)
|
|
.copied()
|
|
{
|
|
Some(2) => Some(RealGroupedTargetSubject::Chairman),
|
|
_ => Some(RealGroupedTargetSubject::Player),
|
|
},
|
|
_ if row.descriptor_id == 3 => Some(RealGroupedTargetSubject::Territory),
|
|
_ if row.descriptor_id == 15
|
|
&& compact_control
|
|
.grouped_territory_selectors_0x80f
|
|
.get(row.group_index)
|
|
.is_some_and(|selector| *selector >= 0) =>
|
|
{
|
|
Some(RealGroupedTargetSubject::Territory)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn real_grouped_target_subject_name(subject: RealGroupedTargetSubject) -> &'static str {
|
|
match subject {
|
|
RealGroupedTargetSubject::Company => "company",
|
|
RealGroupedTargetSubject::Player => "player",
|
|
RealGroupedTargetSubject::Chairman => "chairman",
|
|
RealGroupedTargetSubject::Territory => "territory",
|
|
RealGroupedTargetSubject::WholeGame => "whole_game",
|
|
}
|
|
}
|
|
|
|
fn derive_real_grouped_target_scope_name(
|
|
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
|
|
compact_control: &SmpLoadedPackedEventCompactControlSummary,
|
|
target_subject: Option<RealGroupedTargetSubject>,
|
|
target_scope_ordinal: Option<u8>,
|
|
) -> Option<String> {
|
|
match target_subject {
|
|
Some(RealGroupedTargetSubject::Company) => target_scope_ordinal
|
|
.map(real_grouped_company_scope_name)
|
|
.map(str::to_string),
|
|
Some(RealGroupedTargetSubject::Player) => target_scope_ordinal
|
|
.map(real_grouped_player_scope_name)
|
|
.map(str::to_string),
|
|
Some(RealGroupedTargetSubject::Chairman) => target_scope_ordinal
|
|
.map(real_grouped_chairman_scope_name)
|
|
.map(str::to_string),
|
|
Some(RealGroupedTargetSubject::Territory) => compact_control
|
|
.grouped_territory_selectors_0x80f
|
|
.get(row.group_index)
|
|
.copied()
|
|
.filter(|selector| *selector >= 0)
|
|
.map(|_| "specified_territories".to_string()),
|
|
Some(RealGroupedTargetSubject::WholeGame) => Some("whole_game".to_string()),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
fn real_grouped_company_scope_name(ordinal: u8) -> &'static str {
|
|
match ordinal {
|
|
0 => "condition_true_company",
|
|
1 => "selected_company",
|
|
2 => "human_companies",
|
|
3 => "ai_companies",
|
|
_ => "unsupported_company_scope",
|
|
}
|
|
}
|
|
|
|
fn real_grouped_player_scope_name(ordinal: u8) -> &'static str {
|
|
match ordinal {
|
|
0 => "condition_true_player",
|
|
1 => "selected_player",
|
|
2 => "human_players",
|
|
3 => "ai_players",
|
|
_ => "unsupported_player_scope",
|
|
}
|
|
}
|
|
|
|
fn real_grouped_chairman_scope_name(ordinal: u8) -> &'static str {
|
|
match ordinal {
|
|
0 => "condition_true_chairman",
|
|
1 => "selected_chairman",
|
|
2 => "human_chairmen",
|
|
3 => "ai_chairmen",
|
|
_ => "unsupported_chairman_scope",
|
|
}
|
|
}
|
|
|
|
fn runtime_variable_index(descriptor_id: u32) -> Option<u32> {
|
|
match descriptor_id {
|
|
39..=42 => Some(descriptor_id - 38),
|
|
43..=46 => Some(descriptor_id - 42),
|
|
47..=50 => Some(descriptor_id - 46),
|
|
51..=54 => Some(descriptor_id - 50),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn decode_real_grouped_effect_actions(
|
|
grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary],
|
|
compact_control: &SmpLoadedPackedEventCompactControlSummary,
|
|
) -> Vec<RuntimeEffect> {
|
|
grouped_effect_rows
|
|
.iter()
|
|
.filter_map(|row| decode_real_grouped_effect_action(row, compact_control))
|
|
.collect()
|
|
}
|
|
|
|
fn decode_real_grouped_effect_action(
|
|
row: &SmpLoadedPackedEventGroupedEffectRowSummary,
|
|
compact_control: &SmpLoadedPackedEventCompactControlSummary,
|
|
) -> Option<RuntimeEffect> {
|
|
let descriptor_metadata = real_grouped_effect_descriptor_metadata(row.descriptor_id)?;
|
|
let target_scope_ordinal = compact_control
|
|
.grouped_target_scope_ordinals_0x7fb
|
|
.get(row.group_index)
|
|
.copied()?;
|
|
let target_subject = derive_real_grouped_target_subject(row, compact_control);
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.parameter_family == "runtime_variable_scalar"
|
|
&& row.row_shape == "scalar_assignment"
|
|
{
|
|
let index = runtime_variable_index(descriptor_metadata.descriptor_id)?;
|
|
return match target_subject {
|
|
Some(RealGroupedTargetSubject::WholeGame) => Some(RuntimeEffect::SetWorldVariable {
|
|
index,
|
|
value: i64::from(row.raw_scalar_value),
|
|
}),
|
|
Some(RealGroupedTargetSubject::Company) => Some(RuntimeEffect::SetCompanyVariable {
|
|
target: real_grouped_company_target(target_scope_ordinal)?,
|
|
index,
|
|
value: i64::from(row.raw_scalar_value),
|
|
}),
|
|
Some(RealGroupedTargetSubject::Player) => Some(RuntimeEffect::SetPlayerVariable {
|
|
target: real_grouped_player_target(target_scope_ordinal)?,
|
|
index,
|
|
value: i64::from(row.raw_scalar_value),
|
|
}),
|
|
Some(RealGroupedTargetSubject::Territory) => compact_control
|
|
.grouped_territory_selectors_0x80f
|
|
.get(row.group_index)
|
|
.copied()
|
|
.filter(|selector| *selector >= 0)
|
|
.map(|selector| RuntimeEffect::SetTerritoryVariable {
|
|
target: RuntimeTerritoryTarget::Ids {
|
|
ids: vec![selector as u32],
|
|
},
|
|
index,
|
|
value: i64::from(row.raw_scalar_value),
|
|
}),
|
|
_ => None,
|
|
};
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.parameter_family == "company_governance_scalar"
|
|
&& row.row_shape == "scalar_assignment"
|
|
{
|
|
let target = real_grouped_company_target(target_scope_ordinal)?;
|
|
let metric = real_grouped_company_governance_metric(descriptor_metadata)?;
|
|
return Some(RuntimeEffect::SetCompanyGovernanceScalar {
|
|
target,
|
|
metric,
|
|
value: i64::from(row.raw_scalar_value),
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 1
|
|
&& row.opcode == 8
|
|
&& row.row_shape == "multivalue_scalar"
|
|
{
|
|
return match target_subject {
|
|
Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::SetChairmanCash {
|
|
target: real_grouped_chairman_target(target_scope_ordinal)?,
|
|
value: i64::from(row.raw_scalar_value),
|
|
}),
|
|
_ => Some(RuntimeEffect::SetPlayerCash {
|
|
target: real_grouped_player_target(target_scope_ordinal)?,
|
|
value: i64::from(row.raw_scalar_value),
|
|
}),
|
|
};
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 2
|
|
&& row.opcode == 8
|
|
&& row.row_shape == "multivalue_scalar"
|
|
{
|
|
let target = real_grouped_company_target(target_scope_ordinal)?;
|
|
return Some(RuntimeEffect::SetCompanyCash {
|
|
target,
|
|
value: i64::from(row.raw_scalar_value),
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 3
|
|
&& row.row_shape == "bool_toggle"
|
|
&& row.raw_scalar_value != 0
|
|
{
|
|
let target = real_grouped_company_target(target_scope_ordinal)?;
|
|
let territory = compact_control
|
|
.grouped_territory_selectors_0x80f
|
|
.get(row.group_index)
|
|
.copied()
|
|
.filter(|selector| *selector >= 0)
|
|
.map(|selector| RuntimeTerritoryTarget::Ids {
|
|
ids: vec![selector as u32],
|
|
})?;
|
|
return Some(RuntimeEffect::SetCompanyTerritoryAccess {
|
|
target,
|
|
territory,
|
|
value: true,
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 8
|
|
&& row.row_shape == "scalar_assignment"
|
|
{
|
|
return Some(RuntimeEffect::SetEconomicStatusCode {
|
|
value: row.raw_scalar_value,
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 108
|
|
&& row.row_shape == "scalar_assignment"
|
|
{
|
|
return Some(RuntimeEffect::SetSpecialCondition {
|
|
label: descriptor_metadata.label.to_string(),
|
|
value: row.raw_scalar_value as u32,
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 109
|
|
&& row.row_shape == "scalar_assignment"
|
|
{
|
|
return Some(RuntimeEffect::SetCandidateAvailability {
|
|
name: runtime_candidate_availability_name(descriptor_metadata.label),
|
|
value: row.raw_scalar_value as u32,
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 122
|
|
&& row.row_shape == "scalar_assignment"
|
|
{
|
|
return Some(RuntimeEffect::SetLimitedTrackBuildingAmount {
|
|
value: row.raw_scalar_value,
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.parameter_family == "world_scalar_override"
|
|
&& row.row_shape == "scalar_assignment"
|
|
{
|
|
return Some(RuntimeEffect::SetWorldScalarOverride {
|
|
key: runtime_world_scalar_key(descriptor_metadata)?,
|
|
value: i64::from(row.raw_scalar_value),
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.parameter_family == "cargo_price_scalar"
|
|
&& row.row_shape == "scalar_assignment"
|
|
&& row.raw_scalar_value >= 0
|
|
{
|
|
return match row.descriptor_id {
|
|
105 => Some(RuntimeEffect::SetCargoPriceOverride {
|
|
target: RuntimeCargoPriceTarget::All,
|
|
value: row.raw_scalar_value as u32,
|
|
}),
|
|
descriptor_id => grounded_named_cargo_price_label(descriptor_id).map(|name| {
|
|
RuntimeEffect::SetCargoPriceOverride {
|
|
target: RuntimeCargoPriceTarget::Named {
|
|
name: name.to_string(),
|
|
},
|
|
value: row.raw_scalar_value as u32,
|
|
}
|
|
}),
|
|
};
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.parameter_family == "cargo_production_scalar"
|
|
&& row.row_shape == "scalar_assignment"
|
|
&& row.raw_scalar_value >= 0
|
|
{
|
|
return match descriptor_metadata.descriptor_id {
|
|
177 => Some(RuntimeEffect::SetCargoProductionOverride {
|
|
target: RuntimeCargoProductionTarget::All,
|
|
value: row.raw_scalar_value as u32,
|
|
}),
|
|
178 => Some(RuntimeEffect::SetCargoProductionOverride {
|
|
target: RuntimeCargoProductionTarget::Factory,
|
|
value: row.raw_scalar_value as u32,
|
|
}),
|
|
179 => Some(RuntimeEffect::SetCargoProductionOverride {
|
|
target: RuntimeCargoProductionTarget::FarmMine,
|
|
value: row.raw_scalar_value as u32,
|
|
}),
|
|
230..=240 => {
|
|
let slot = descriptor_metadata.descriptor_id.checked_sub(229)?;
|
|
Some(RuntimeEffect::SetCargoProductionSlot {
|
|
slot,
|
|
value: row.raw_scalar_value as u32,
|
|
})
|
|
}
|
|
_ => None,
|
|
};
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.parameter_family == "territory_access_cost_scalar"
|
|
&& row.row_shape == "scalar_assignment"
|
|
&& row.raw_scalar_value >= 0
|
|
{
|
|
return Some(RuntimeEffect::SetTerritoryAccessCost {
|
|
value: row.raw_scalar_value as u32,
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.parameter_family == "world_flag_toggle"
|
|
&& row.row_shape == "bool_toggle"
|
|
{
|
|
return Some(RuntimeEffect::SetWorldFlag {
|
|
key: runtime_world_flag_key(descriptor_metadata)?,
|
|
value: row.raw_scalar_value != 0,
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 9
|
|
&& row.row_shape == "bool_toggle"
|
|
&& row.raw_scalar_value != 0
|
|
{
|
|
let target = real_grouped_company_target(target_scope_ordinal)?;
|
|
return Some(RuntimeEffect::ConfiscateCompanyAssets { target });
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 13
|
|
&& row.row_shape == "bool_toggle"
|
|
&& row.raw_scalar_value != 0
|
|
{
|
|
let target = real_grouped_company_target(target_scope_ordinal)?;
|
|
return Some(RuntimeEffect::DeactivateCompany { target });
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 14
|
|
&& row.row_shape == "bool_toggle"
|
|
&& row.raw_scalar_value != 0
|
|
{
|
|
return match target_subject {
|
|
Some(RealGroupedTargetSubject::Chairman) => Some(RuntimeEffect::DeactivateChairman {
|
|
target: real_grouped_chairman_target(target_scope_ordinal)?,
|
|
}),
|
|
_ => Some(RuntimeEffect::DeactivatePlayer {
|
|
target: real_grouped_player_target(target_scope_ordinal)?,
|
|
}),
|
|
};
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 16
|
|
&& row.row_shape == "scalar_assignment"
|
|
&& row.raw_scalar_value >= 0
|
|
{
|
|
let target = real_grouped_company_target(target_scope_ordinal)?;
|
|
return Some(RuntimeEffect::SetCompanyTrackLayingCapacity {
|
|
target,
|
|
value: Some(row.raw_scalar_value as u32),
|
|
});
|
|
}
|
|
|
|
if descriptor_metadata.executable_in_runtime
|
|
&& descriptor_metadata.descriptor_id == 15
|
|
&& row.row_shape == "bool_toggle"
|
|
&& row.raw_scalar_value != 0
|
|
{
|
|
let company_target = real_grouped_company_target(target_scope_ordinal);
|
|
let territory_target = compact_control
|
|
.grouped_territory_selectors_0x80f
|
|
.get(row.group_index)
|
|
.copied()
|
|
.filter(|selector| *selector >= 0)
|
|
.map(|selector| RuntimeTerritoryTarget::Ids {
|
|
ids: vec![selector as u32],
|
|
});
|
|
if company_target.is_none() && territory_target.is_none() {
|
|
return None;
|
|
}
|
|
return Some(RuntimeEffect::RetireTrains {
|
|
company_target,
|
|
territory_target,
|
|
locomotive_name: row.locomotive_name.clone(),
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn real_grouped_company_target(ordinal: u8) -> Option<RuntimeCompanyTarget> {
|
|
match ordinal {
|
|
0 => Some(RuntimeCompanyTarget::ConditionTrueCompany),
|
|
1 => Some(RuntimeCompanyTarget::SelectedCompany),
|
|
2 => Some(RuntimeCompanyTarget::HumanCompanies),
|
|
3 => Some(RuntimeCompanyTarget::AiCompanies),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn real_grouped_player_target(ordinal: u8) -> Option<RuntimePlayerTarget> {
|
|
match ordinal {
|
|
0 => Some(RuntimePlayerTarget::ConditionTruePlayer),
|
|
1 => Some(RuntimePlayerTarget::SelectedPlayer),
|
|
2 => Some(RuntimePlayerTarget::HumanPlayers),
|
|
3 => Some(RuntimePlayerTarget::AiPlayers),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn real_grouped_chairman_target(ordinal: u8) -> Option<RuntimeChairmanTarget> {
|
|
match ordinal {
|
|
0 => Some(RuntimeChairmanTarget::ConditionTrueChairman),
|
|
1 => Some(RuntimeChairmanTarget::SelectedChairman),
|
|
2 => Some(RuntimeChairmanTarget::HumanChairmen),
|
|
3 => Some(RuntimeChairmanTarget::AiChairmen),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn real_grouped_chairman_target_supported_in_runtime(ordinal: u8) -> bool {
|
|
real_grouped_chairman_target(ordinal).is_some()
|
|
}
|
|
|
|
fn parse_synthetic_packed_event_action(bytes: &[u8], cursor: &mut usize) -> Option<RuntimeEffect> {
|
|
let opcode = read_u8_at(bytes, *cursor)?;
|
|
*cursor += 1;
|
|
match opcode {
|
|
0x01 => {
|
|
let key = parse_len_prefixed_string(bytes, cursor)?;
|
|
let value = read_u8_at(bytes, *cursor)? != 0;
|
|
*cursor += 1;
|
|
Some(RuntimeEffect::SetWorldFlag { key, value })
|
|
}
|
|
0x02 => {
|
|
let target = parse_synthetic_company_target(bytes, cursor)?;
|
|
let delta = read_i64_at(bytes, *cursor)?;
|
|
*cursor += 8;
|
|
Some(RuntimeEffect::AdjustCompanyCash { target, delta })
|
|
}
|
|
0x03 => {
|
|
let target = parse_synthetic_company_target(bytes, cursor)?;
|
|
let delta = read_i64_at(bytes, *cursor)?;
|
|
*cursor += 8;
|
|
Some(RuntimeEffect::AdjustCompanyDebt { target, delta })
|
|
}
|
|
0x04 => {
|
|
let name = parse_len_prefixed_string(bytes, cursor)?;
|
|
let value = read_u32_at(bytes, *cursor)?;
|
|
*cursor += 4;
|
|
Some(RuntimeEffect::SetCandidateAvailability { name, value })
|
|
}
|
|
0x05 => {
|
|
let label = parse_len_prefixed_string(bytes, cursor)?;
|
|
let value = read_u32_at(bytes, *cursor)?;
|
|
*cursor += 4;
|
|
Some(RuntimeEffect::SetSpecialCondition { label, value })
|
|
}
|
|
0x06 => {
|
|
let template_len = usize::try_from(read_u32_at(bytes, *cursor)?).ok()?;
|
|
*cursor += 4;
|
|
let template_bytes = bytes.get(*cursor..*cursor + template_len)?;
|
|
let record = parse_synthetic_event_runtime_record_template(template_bytes)?;
|
|
*cursor += template_len;
|
|
Some(RuntimeEffect::AppendEventRecord {
|
|
record: Box::new(record),
|
|
})
|
|
}
|
|
0x07 => {
|
|
let record_id = read_u32_at(bytes, *cursor)?;
|
|
*cursor += 4;
|
|
Some(RuntimeEffect::ActivateEventRecord { record_id })
|
|
}
|
|
0x08 => {
|
|
let record_id = read_u32_at(bytes, *cursor)?;
|
|
*cursor += 4;
|
|
Some(RuntimeEffect::DeactivateEventRecord { record_id })
|
|
}
|
|
0x09 => {
|
|
let record_id = read_u32_at(bytes, *cursor)?;
|
|
*cursor += 4;
|
|
Some(RuntimeEffect::RemoveEventRecord { record_id })
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn parse_synthetic_event_runtime_record_template(
|
|
bytes: &[u8],
|
|
) -> Option<RuntimeEventRecordTemplate> {
|
|
if !bytes.starts_with(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC) {
|
|
return None;
|
|
}
|
|
|
|
let mut cursor = PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC.len();
|
|
let record_id = read_u32_at(bytes, cursor)?;
|
|
cursor += 4;
|
|
let trigger_kind = read_u8_at(bytes, cursor)?;
|
|
cursor += 1;
|
|
let flags = read_u8_at(bytes, cursor)?;
|
|
cursor += 1;
|
|
let action_count = usize::from(read_u8_at(bytes, cursor)?);
|
|
cursor += 1;
|
|
cursor += 1;
|
|
|
|
let mut effects = Vec::with_capacity(action_count);
|
|
for _ in 0..action_count {
|
|
effects.push(parse_synthetic_packed_event_action(bytes, &mut cursor)?);
|
|
}
|
|
|
|
if cursor != bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
Some(RuntimeEventRecordTemplate {
|
|
record_id,
|
|
trigger_kind,
|
|
active: flags & 0x01 != 0,
|
|
marks_collection_dirty: flags & 0x02 != 0,
|
|
one_shot: flags & 0x04 != 0,
|
|
conditions: Vec::new(),
|
|
effects,
|
|
})
|
|
}
|
|
|
|
fn parse_synthetic_company_target(
|
|
bytes: &[u8],
|
|
cursor: &mut usize,
|
|
) -> Option<RuntimeCompanyTarget> {
|
|
let target_kind = read_u8_at(bytes, *cursor)?;
|
|
*cursor += 1;
|
|
match target_kind {
|
|
0x00 => Some(RuntimeCompanyTarget::AllActive),
|
|
0x01 => {
|
|
let count = usize::from(read_u8_at(bytes, *cursor)?);
|
|
*cursor += 1;
|
|
let mut ids = Vec::with_capacity(count);
|
|
for _ in 0..count {
|
|
ids.push(read_u32_at(bytes, *cursor)?);
|
|
*cursor += 4;
|
|
}
|
|
Some(RuntimeCompanyTarget::Ids { ids })
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn parse_len_prefixed_string(bytes: &[u8], cursor: &mut usize) -> Option<String> {
|
|
let len = usize::from(read_u8_at(bytes, *cursor)?);
|
|
*cursor += 1;
|
|
let text_bytes = bytes.get(*cursor..*cursor + len)?;
|
|
*cursor += len;
|
|
Some(String::from_utf8_lossy(text_bytes).into_owned())
|
|
}
|
|
|
|
fn parse_optional_u16_len_prefixed_string(
|
|
bytes: &[u8],
|
|
cursor: &mut usize,
|
|
) -> Option<Option<String>> {
|
|
let len = usize::from(read_u16_at(bytes, *cursor)?);
|
|
*cursor += 2;
|
|
if len == 0 {
|
|
return Some(None);
|
|
}
|
|
let text_bytes = bytes.get(*cursor..*cursor + len)?;
|
|
*cursor += len;
|
|
Some(Some(String::from_utf8_lossy(text_bytes).into_owned()))
|
|
}
|
|
|
|
fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
|
|
match effect {
|
|
RuntimeEffect::SetChairmanCash { target, .. }
|
|
| RuntimeEffect::DeactivateChairman { target } => matches!(
|
|
target,
|
|
RuntimeChairmanTarget::AllActive
|
|
| RuntimeChairmanTarget::HumanChairmen
|
|
| RuntimeChairmanTarget::AiChairmen
|
|
| RuntimeChairmanTarget::SelectedChairman
|
|
| RuntimeChairmanTarget::ConditionTrueChairman
|
|
| RuntimeChairmanTarget::Ids { .. }
|
|
),
|
|
RuntimeEffect::SetWorldFlag { .. }
|
|
| RuntimeEffect::SetWorldVariable { .. }
|
|
| RuntimeEffect::SetLimitedTrackBuildingAmount { .. }
|
|
| RuntimeEffect::SetEconomicStatusCode { .. }
|
|
| RuntimeEffect::SetCompanyGovernanceScalar { .. }
|
|
| RuntimeEffect::SetCandidateAvailability { .. }
|
|
| RuntimeEffect::SetNamedLocomotiveAvailability { .. }
|
|
| RuntimeEffect::SetNamedLocomotiveAvailabilityValue { .. }
|
|
| RuntimeEffect::SetNamedLocomotiveCost { .. }
|
|
| RuntimeEffect::SetCargoPriceOverride { .. }
|
|
| RuntimeEffect::SetCargoProductionOverride { .. }
|
|
| RuntimeEffect::SetCargoProductionSlot { .. }
|
|
| RuntimeEffect::SetWorldScalarOverride { .. }
|
|
| RuntimeEffect::SetTerritoryAccessCost { .. }
|
|
| RuntimeEffect::SetSpecialCondition { .. }
|
|
| RuntimeEffect::ConfiscateCompanyAssets { .. }
|
|
| RuntimeEffect::DeactivateCompany { .. }
|
|
| RuntimeEffect::DeactivatePlayer { .. }
|
|
| RuntimeEffect::SetCompanyTrackLayingCapacity { .. }
|
|
| RuntimeEffect::RetireTrains { .. }
|
|
| RuntimeEffect::ActivateEventRecord { .. }
|
|
| RuntimeEffect::DeactivateEventRecord { .. }
|
|
| RuntimeEffect::RemoveEventRecord { .. } => true,
|
|
RuntimeEffect::SetPlayerCash { target, .. }
|
|
| RuntimeEffect::SetPlayerVariable { target, .. } => matches!(
|
|
target,
|
|
RuntimePlayerTarget::AllActive
|
|
| RuntimePlayerTarget::Ids { .. }
|
|
| RuntimePlayerTarget::HumanPlayers
|
|
| RuntimePlayerTarget::AiPlayers
|
|
| RuntimePlayerTarget::SelectedPlayer
|
|
| RuntimePlayerTarget::ConditionTruePlayer
|
|
),
|
|
RuntimeEffect::SetCompanyTerritoryAccess {
|
|
target, territory, ..
|
|
} => {
|
|
matches!(
|
|
target,
|
|
RuntimeCompanyTarget::AllActive
|
|
| RuntimeCompanyTarget::Ids { .. }
|
|
| RuntimeCompanyTarget::HumanCompanies
|
|
| RuntimeCompanyTarget::AiCompanies
|
|
| RuntimeCompanyTarget::SelectedCompany
|
|
| RuntimeCompanyTarget::ConditionTrueCompany
|
|
) && matches!(
|
|
territory,
|
|
RuntimeTerritoryTarget::AllTerritories | RuntimeTerritoryTarget::Ids { .. }
|
|
)
|
|
}
|
|
RuntimeEffect::SetCompanyCash { target, .. }
|
|
| RuntimeEffect::SetCompanyVariable { target, .. }
|
|
| RuntimeEffect::AdjustCompanyCash { target, .. }
|
|
| RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!(
|
|
target,
|
|
RuntimeCompanyTarget::AllActive
|
|
| RuntimeCompanyTarget::Ids { .. }
|
|
| RuntimeCompanyTarget::HumanCompanies
|
|
| RuntimeCompanyTarget::AiCompanies
|
|
| RuntimeCompanyTarget::SelectedCompany
|
|
| RuntimeCompanyTarget::ConditionTrueCompany
|
|
),
|
|
RuntimeEffect::SetTerritoryVariable { target, .. } => matches!(
|
|
target,
|
|
RuntimeTerritoryTarget::AllTerritories | RuntimeTerritoryTarget::Ids { .. }
|
|
),
|
|
RuntimeEffect::AppendEventRecord { record } => record
|
|
.effects
|
|
.iter()
|
|
.all(runtime_effect_supported_for_save_import),
|
|
}
|
|
}
|
|
|
|
fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> bool {
|
|
match condition {
|
|
RuntimeCondition::WorldVariableThreshold { .. }
|
|
| RuntimeCondition::CompanyNumericThreshold { .. }
|
|
| RuntimeCondition::CompanyVariableThreshold { .. }
|
|
| RuntimeCondition::PlayerVariableThreshold { .. }
|
|
| RuntimeCondition::ChairmanNumericThreshold { .. }
|
|
| RuntimeCondition::TerritoryNumericThreshold { .. }
|
|
| RuntimeCondition::TerritoryVariableThreshold { .. }
|
|
| RuntimeCondition::CompanyTerritoryNumericThreshold { .. }
|
|
| RuntimeCondition::SpecialConditionThreshold { .. }
|
|
| RuntimeCondition::CandidateAvailabilityThreshold { .. }
|
|
| RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. }
|
|
| RuntimeCondition::NamedLocomotiveCostThreshold { .. }
|
|
| RuntimeCondition::CargoProductionSlotThreshold { .. }
|
|
| RuntimeCondition::CargoProductionTotalThreshold { .. }
|
|
| RuntimeCondition::FactoryProductionTotalThreshold { .. }
|
|
| RuntimeCondition::FarmMineProductionTotalThreshold { .. }
|
|
| RuntimeCondition::OtherCargoProductionTotalThreshold { .. }
|
|
| RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. }
|
|
| RuntimeCondition::TerritoryAccessCostThreshold { .. }
|
|
| RuntimeCondition::EconomicStatusCodeThreshold { .. }
|
|
| RuntimeCondition::WorldFlagEquals { .. } => true,
|
|
}
|
|
}
|
|
|
|
fn build_unsupported_event_runtime_record_summaries(
|
|
live_entry_ids: &[u32],
|
|
note: &str,
|
|
) -> Vec<SmpLoadedPackedEventRecordSummary> {
|
|
live_entry_ids
|
|
.iter()
|
|
.copied()
|
|
.enumerate()
|
|
.map(
|
|
|(record_index, live_entry_id)| SmpLoadedPackedEventRecordSummary {
|
|
record_index,
|
|
live_entry_id,
|
|
payload_offset: None,
|
|
payload_len: None,
|
|
decode_status: "unsupported_framing".to_string(),
|
|
payload_family: "unsupported_framing".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: vec![note.to_string()],
|
|
},
|
|
)
|
|
.collect()
|
|
}
|
|
|
|
fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> SmpInspectionReport {
|
|
let known_tag_hits = KNOWN_TAG_DEFINITIONS
|
|
.iter()
|
|
.filter_map(|definition| {
|
|
let offsets = find_u16_le_offsets(bytes, definition.tag_id);
|
|
if offsets.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some(SmpKnownTagHit {
|
|
tag_id: definition.tag_id,
|
|
tag_hex: format!("0x{:04x}", definition.tag_id),
|
|
label: definition.label.to_string(),
|
|
grounded_meaning: definition.grounded_meaning.to_string(),
|
|
hit_count: offsets.len(),
|
|
sample_offsets: offsets
|
|
.iter()
|
|
.copied()
|
|
.take(TAG_OFFSET_SAMPLE_LIMIT)
|
|
.collect(),
|
|
last_offset: offsets.last().copied(),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let shared_header = parse_shared_header(bytes);
|
|
let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe);
|
|
let first_ascii_run = find_first_ascii_run(bytes);
|
|
let early_content_probe = first_ascii_run
|
|
.as_ref()
|
|
.and_then(|ascii_run| probe_early_content_layout(bytes, ascii_run));
|
|
let secondary_variant_probe = early_content_probe
|
|
.as_ref()
|
|
.and_then(classify_secondary_variant_probe);
|
|
let container_profile = classify_container_profile(
|
|
file_extension_hint.as_deref(),
|
|
header_variant_probe.as_ref(),
|
|
secondary_variant_probe.as_ref(),
|
|
);
|
|
let runtime_anchor_cycle_block = parse_runtime_anchor_cycle_block(
|
|
bytes,
|
|
container_profile.as_ref(),
|
|
secondary_variant_probe.as_ref(),
|
|
);
|
|
let save_bootstrap_block =
|
|
parse_save_bootstrap_block(container_profile.as_ref(), secondary_variant_probe.as_ref());
|
|
let save_anchor_run_block = parse_save_anchor_run_block(
|
|
bytes,
|
|
container_profile.as_ref(),
|
|
save_bootstrap_block.as_ref(),
|
|
);
|
|
let runtime_trailer_block = parse_runtime_trailer_block(
|
|
container_profile.as_ref(),
|
|
runtime_anchor_cycle_block.as_ref(),
|
|
);
|
|
let runtime_post_span_probe =
|
|
parse_runtime_post_span_probe(bytes, runtime_trailer_block.as_ref());
|
|
let rt3_105_packed_profile_probe = parse_rt3_105_packed_profile_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
header_variant_probe.as_ref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let rt3_105_post_span_bridge_probe = parse_rt3_105_post_span_bridge_probe(
|
|
runtime_trailer_block.as_ref(),
|
|
runtime_post_span_probe.as_ref(),
|
|
rt3_105_packed_profile_probe.as_ref(),
|
|
);
|
|
let rt3_105_save_bridge_payload_probe =
|
|
parse_rt3_105_save_bridge_payload_probe(bytes, rt3_105_post_span_bridge_probe.as_ref());
|
|
let save_world_selection_context_probe = parse_save_world_selection_context_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_world_issue_37_probe = parse_save_world_issue_37_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_world_economic_tuning_probe = parse_save_world_economic_tuning_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_world_finance_neighborhood_probe = parse_save_world_finance_neighborhood_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_company_collection_header_probe = parse_save_company_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_chairman_profile_collection_header_probe =
|
|
parse_save_chairman_profile_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_train_collection_header_probe = parse_save_train_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_train_collection_directory_probe = parse_save_train_collection_directory_probe(
|
|
bytes,
|
|
save_train_collection_header_probe.as_ref(),
|
|
);
|
|
let save_region_collection_header_probe = parse_save_region_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_region_record_triplet_probe =
|
|
parse_save_region_record_triplet_probe(bytes, save_region_collection_header_probe.as_ref());
|
|
let save_region_queued_notice_record_probe = parse_save_region_queued_notice_record_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
save_region_collection_header_probe.as_ref(),
|
|
);
|
|
let save_placed_structure_collection_header_probe =
|
|
parse_save_placed_structure_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let save_placed_structure_record_triplet_probe =
|
|
parse_save_placed_structure_record_triplet_probe(
|
|
bytes,
|
|
save_placed_structure_collection_header_probe.as_ref(),
|
|
);
|
|
let save_placed_structure_dynamic_side_buffer_probe =
|
|
parse_save_placed_structure_dynamic_side_buffer_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let known_header_probes = [
|
|
save_company_collection_header_probe.as_ref(),
|
|
save_chairman_profile_collection_header_probe.as_ref(),
|
|
save_train_collection_header_probe.as_ref(),
|
|
save_region_collection_header_probe.as_ref(),
|
|
save_placed_structure_collection_header_probe.as_ref(),
|
|
];
|
|
let save_unclassified_tagged_collection_header_probes =
|
|
filter_unclassified_tagged_collection_header_probes_outside_known_spans(
|
|
scan_save_unclassified_tagged_collection_header_probes(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
),
|
|
&known_header_probes,
|
|
);
|
|
let save_company_roster_probe = parse_save_company_roster_probe(
|
|
bytes,
|
|
save_company_collection_header_probe.as_ref(),
|
|
save_world_selection_context_probe.as_ref(),
|
|
);
|
|
let save_chairman_profile_table_probe = parse_save_chairman_profile_table_probe(
|
|
bytes,
|
|
save_chairman_profile_collection_header_probe.as_ref(),
|
|
save_world_selection_context_probe.as_ref(),
|
|
save_company_collection_header_probe.as_ref(),
|
|
);
|
|
let rt3_105_save_name_table_probe = parse_rt3_105_save_name_table_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
rt3_105_save_bridge_payload_probe.as_ref(),
|
|
);
|
|
let rt3_105_save_named_locomotive_availability_probe =
|
|
parse_rt3_105_save_named_locomotive_availability_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
rt3_105_packed_profile_probe.as_ref(),
|
|
);
|
|
let special_conditions_probe = parse_special_conditions_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
);
|
|
let smp_aligned_runtime_rule_band_probe = parse_smp_aligned_runtime_rule_band_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
special_conditions_probe.as_ref(),
|
|
);
|
|
let post_special_conditions_scalar_probe = parse_post_special_conditions_scalar_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
special_conditions_probe.as_ref(),
|
|
);
|
|
let post_text_field_neighborhood_probe = parse_post_text_field_neighborhood_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
special_conditions_probe.as_ref(),
|
|
);
|
|
let locomotive_policy_neighborhood_probe = parse_locomotive_policy_neighborhood_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
special_conditions_probe.as_ref(),
|
|
);
|
|
let pre_recipe_scalar_plateau_probe = parse_pre_recipe_scalar_plateau_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
special_conditions_probe.as_ref(),
|
|
);
|
|
let recipe_book_summary_probe = parse_recipe_book_summary_probe(
|
|
bytes,
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
special_conditions_probe.as_ref(),
|
|
);
|
|
let classic_rehydrate_profile_probe =
|
|
parse_classic_rehydrate_profile_probe(bytes, runtime_post_span_probe.as_ref());
|
|
let save_load_summary = build_save_load_summary(
|
|
file_extension_hint.as_deref(),
|
|
container_profile.as_ref(),
|
|
runtime_trailer_block.as_ref(),
|
|
rt3_105_post_span_bridge_probe.as_ref(),
|
|
classic_rehydrate_profile_probe.as_ref(),
|
|
rt3_105_packed_profile_probe.as_ref(),
|
|
rt3_105_save_name_table_probe.as_ref(),
|
|
);
|
|
let event_runtime_collection_summary = parse_event_runtime_collection_summary(
|
|
bytes,
|
|
container_profile.as_ref(),
|
|
save_load_summary.as_ref(),
|
|
);
|
|
let mut warnings = Vec::new();
|
|
if bytes.is_empty() {
|
|
warnings
|
|
.push("File is empty, so no `.smp` container structure could be observed.".to_string());
|
|
}
|
|
|
|
if known_tag_hits.is_empty() {
|
|
warnings.push(
|
|
"No grounded runtime bundle tags were found in little-endian form. This does not prove the file is invalid."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if shared_header.is_none() && !bytes.is_empty() {
|
|
warnings.push(
|
|
"File is shorter than the observed 64-byte common RT3 bundle preamble.".to_string(),
|
|
);
|
|
}
|
|
if let Some(shared_header) = &shared_header {
|
|
let header_family_is_known = header_variant_probe
|
|
.as_ref()
|
|
.map(|probe| probe.is_known_family)
|
|
.unwrap_or(false);
|
|
if !shared_header.matches_grounded_common_signature && !header_family_is_known {
|
|
warnings.push(
|
|
"The first 64-byte preamble does not match the currently observed shared RT3 bundle signature."
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
if first_ascii_run.is_some() && early_content_probe.is_none() {
|
|
warnings.push(
|
|
"Found early text content but could not resolve the next stable nonzero region after its zero padding."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if container_profile
|
|
.as_ref()
|
|
.is_some_and(|profile| !profile.is_known_profile)
|
|
{
|
|
warnings.push(
|
|
"The current probes did not match any known composite container profile.".to_string(),
|
|
);
|
|
}
|
|
if known_tag_hits
|
|
.iter()
|
|
.any(|hit| hit.hit_count > hit.sample_offsets.len())
|
|
{
|
|
warnings.push(
|
|
"Known-tag offsets are sampled in this report. Large hit counts usually mean byte-pattern noise, not validated chunk boundaries."
|
|
.to_string(),
|
|
);
|
|
}
|
|
warnings.push(
|
|
"Inspection scans raw bytes for a small grounded tag set only. It does not validate bundle layout or decode payloads."
|
|
.to_string(),
|
|
);
|
|
|
|
SmpInspectionReport {
|
|
inspection_mode: "grounded-tag-scan-plus-preamble".to_string(),
|
|
file_extension_hint,
|
|
file_size: bytes.len(),
|
|
sha256: sha256_hex(bytes),
|
|
preamble: parse_preamble(bytes),
|
|
shared_header,
|
|
header_variant_probe,
|
|
first_ascii_run,
|
|
early_content_probe,
|
|
secondary_variant_probe,
|
|
container_profile,
|
|
save_bootstrap_block,
|
|
save_anchor_run_block,
|
|
runtime_anchor_cycle_block,
|
|
runtime_trailer_block,
|
|
runtime_post_span_probe,
|
|
rt3_105_post_span_bridge_probe,
|
|
rt3_105_save_bridge_payload_probe,
|
|
save_world_selection_context_probe,
|
|
save_world_issue_37_probe,
|
|
save_world_economic_tuning_probe,
|
|
save_world_finance_neighborhood_probe,
|
|
save_company_collection_header_probe,
|
|
save_chairman_profile_collection_header_probe,
|
|
save_train_collection_header_probe,
|
|
save_train_collection_directory_probe,
|
|
save_region_collection_header_probe,
|
|
save_region_record_triplet_probe,
|
|
save_region_queued_notice_record_probe,
|
|
save_placed_structure_collection_header_probe,
|
|
save_placed_structure_record_triplet_probe,
|
|
save_placed_structure_dynamic_side_buffer_probe,
|
|
save_unclassified_tagged_collection_header_probes,
|
|
save_company_roster_probe,
|
|
save_chairman_profile_table_probe,
|
|
rt3_105_save_name_table_probe,
|
|
rt3_105_save_named_locomotive_availability_probe,
|
|
special_conditions_probe,
|
|
smp_aligned_runtime_rule_band_probe,
|
|
post_special_conditions_scalar_probe,
|
|
post_text_field_neighborhood_probe,
|
|
locomotive_policy_neighborhood_probe,
|
|
pre_recipe_scalar_plateau_probe,
|
|
recipe_book_summary_probe,
|
|
classic_rehydrate_profile_probe,
|
|
rt3_105_packed_profile_probe,
|
|
save_load_summary,
|
|
event_runtime_collection_summary,
|
|
contains_grounded_runtime_tags: !known_tag_hits.is_empty(),
|
|
known_tag_hits,
|
|
notes: vec![
|
|
"Grounded `.smp` runtime tags currently include mask-plane payload ids 0x2cee and 0x2d51.".to_string(),
|
|
"Grounded sidecar-byte-plane bundle family currently spans 0x9471..0x9472.".to_string(),
|
|
"The shared-header parse is intentionally conservative: it only names common preamble lanes and checks the observed RT3 bundle-family signature.".to_string(),
|
|
"The header-variant probe classifies the preamble into one of the currently observed install-era families when possible."
|
|
.to_string(),
|
|
"The early-content probe resolves the first stable nonzero block after the padded scenario text and then captures the next aligned word window."
|
|
.to_string(),
|
|
"The secondary-variant probe classifies that aligned word window into one of the currently observed file-family patterns."
|
|
.to_string(),
|
|
"The recipe-book summary probe reports per-book structural signatures at the grounded recipe-book root [world+0x0fe7] without attempting a full cargo-line decode."
|
|
.to_string(),
|
|
"Where a recipe cargo-token word looks like two printable letters in its high 16 bits, the probe exposes that as one probable ASCII stem while still treating the wider token semantics as inferred."
|
|
.to_string(),
|
|
"The container-profile layer combines extension hint, header family, and second-window family into one observed container classification."
|
|
.to_string(),
|
|
"The save-bootstrap reader currently parses one conservative 8-word descriptor only for known save-container profiles."
|
|
.to_string(),
|
|
"The save-anchor-run reader follows that descriptor tail into the observed repeated 9-word anchor cycle and captures the first trailer words after the cycle diverges."
|
|
.to_string(),
|
|
"The runtime-anchor-cycle reader applies the same cycle/trailer scan across the currently known save and sandbox runtime container profiles."
|
|
.to_string(),
|
|
"The runtime-trailer reader classifies the first 16 words after the cycle divergence into the currently observed runtime trailer families."
|
|
.to_string(),
|
|
"The runtime post-span probe follows the trailer's high-16 span lane into the later file region and records the next nonzero bytes, the first aligned high-16-dense candidate window, and any grounded progress-id hits found nearby."
|
|
.to_string(),
|
|
"The RT3 1.05 post-span bridge probe correlates the trailer selector/descriptor lanes with the next candidate region and the later packed-profile block for the currently observed 1.05 save families."
|
|
.to_string(),
|
|
"The RT3 1.05 common-save bridge payload probe captures the two stable bridge-stage blocks currently observed under the base 1.05 save branch."
|
|
.to_string(),
|
|
"The RT3 1.05 candidate-availability table probe decodes the fixed-width trailing name table from either the common-save bridge payload or the fixed 0x6a70..0x73c0 source range when that header validates."
|
|
.to_string(),
|
|
"The RT3 1.05 save-side named locomotive availability probe scans the post-profile save region for the grounded fixed-width locomotive-name-plus-dword row family when that run is present."
|
|
.to_string(),
|
|
"The post-special-conditions scalar probe captures the fixed 0x0df4..0x0f30 dword window immediately after the hidden sentinel slot, splits it into the aligned-band overlap prefix and the later tail, and records the live-object offset alignment of that tail without claiming a byte-for-byte mirror."
|
|
.to_string(),
|
|
"The classic rehydrate-profile probe recognizes the grounded 0x32dc -> 0x3714 -> 0x3715 progress-id sequence and captures the exact 0x108-byte block between the latter two ids when that pattern appears."
|
|
.to_string(),
|
|
"The classic packed-profile block reader exposes the stable map-path, display-name, atlas-tracked latch bytes, and the small set of nonzero word lanes observed inside that 0x108-byte block."
|
|
.to_string(),
|
|
"The RT3 1.05 packed-profile probe recognizes the later string-bearing save block rooted at the first post-header .gmp path and exposes the observed map-path, display-name, atlas-tracked byte lanes, and stable nonzero words."
|
|
.to_string(),
|
|
format!(
|
|
"Restore-side loading of the four sidecar byte planes is only grounded for bundle versions >= 0x{:04x}.",
|
|
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION
|
|
),
|
|
],
|
|
warnings,
|
|
}
|
|
}
|
|
|
|
fn build_save_load_summary(
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>,
|
|
rt3_105_post_span_bridge_probe: Option<&SmpRt3105PostSpanBridgeProbe>,
|
|
classic_rehydrate_profile_probe: Option<&SmpClassicRehydrateProfileProbe>,
|
|
rt3_105_packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>,
|
|
rt3_105_save_name_table_probe: Option<&SmpRt3105SaveNameTableProbe>,
|
|
) -> Option<SmpSaveLoadSummary> {
|
|
let file_extension_hint = file_extension_hint.map(str::to_string);
|
|
let container_profile_family = container_profile.map(|profile| profile.profile_family.clone());
|
|
let trailer_family = runtime_trailer_block.map(|trailer| trailer.trailer_family.clone());
|
|
let bridge_family = rt3_105_post_span_bridge_probe.map(|bridge| bridge.bridge_family.clone());
|
|
let candidate_table =
|
|
rt3_105_save_name_table_probe.map(|probe| SmpSaveLoadCandidateTableSummary {
|
|
source_kind: probe.source_kind.clone(),
|
|
semantic_family: probe.semantic_family.clone(),
|
|
observed_entry_count: probe.observed_entry_count,
|
|
zero_availability_count: probe.zero_trailer_entry_count,
|
|
zero_availability_names: probe.zero_trailer_entry_names.clone(),
|
|
footer_progress_hex_words: vec![
|
|
probe.footer_progress_word_0_hex.clone(),
|
|
probe.footer_progress_word_1_hex.clone(),
|
|
],
|
|
});
|
|
|
|
if let Some(probe) = classic_rehydrate_profile_probe {
|
|
let block = &probe.packed_profile_block;
|
|
let mut notes = vec![
|
|
"Classic save load reaches the grounded late rehydrate band 0x32dc -> 0x3714 -> 0x3715."
|
|
.to_string(),
|
|
"The file exposes one exact 0x108 packed-profile block between progress ids 0x3714 and 0x3715."
|
|
.to_string(),
|
|
];
|
|
if let Some(map_path) = &block.map_path {
|
|
notes.push(format!("Packed profile map path: {map_path}"));
|
|
}
|
|
if let Some(display_name) = &block.display_name {
|
|
notes.push(format!("Packed profile display name: {display_name}"));
|
|
}
|
|
|
|
return Some(SmpSaveLoadSummary {
|
|
file_extension_hint,
|
|
container_profile_family,
|
|
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
|
mechanism_confidence: "grounded".to_string(),
|
|
packed_profile_kind: Some("classic-rehydrate-profile".to_string()),
|
|
packed_profile_family: Some(probe.profile_family.clone()),
|
|
packed_profile_offset: Some(probe.packed_profile_offset),
|
|
packed_profile_len: Some(probe.packed_profile_len),
|
|
map_path: block.map_path.clone(),
|
|
display_name: block.display_name.clone(),
|
|
profile_byte_0x77: Some(block.profile_byte_0x77),
|
|
profile_byte_0x77_hex: Some(block.profile_byte_0x77_hex.clone()),
|
|
profile_byte_0x82: Some(block.profile_byte_0x82),
|
|
profile_byte_0x82_hex: Some(block.profile_byte_0x82_hex.clone()),
|
|
profile_byte_0x97: Some(block.profile_byte_0x97),
|
|
profile_byte_0x97_hex: Some(block.profile_byte_0x97_hex.clone()),
|
|
profile_byte_0xc5: Some(block.profile_byte_0xc5),
|
|
profile_byte_0xc5_hex: Some(block.profile_byte_0xc5_hex.clone()),
|
|
trailer_family,
|
|
bridge_family: None,
|
|
candidate_table,
|
|
notes,
|
|
});
|
|
}
|
|
|
|
if let Some(probe) = rt3_105_packed_profile_probe {
|
|
let block = &probe.packed_profile_block;
|
|
let mechanism_family = rt3_105_post_span_bridge_probe
|
|
.map(|bridge| bridge.bridge_family.clone())
|
|
.unwrap_or_else(|| match probe.profile_family.as_str() {
|
|
"rt3-105-scenario-save-container-v1" => {
|
|
"rt3-105-scenario-save-profile-analog-v1".to_string()
|
|
}
|
|
"rt3-105-alt-save-container-v1" => "rt3-105-alt-save-profile-analog-v1".to_string(),
|
|
_ => "rt3-105-save-profile-analog-v1".to_string(),
|
|
});
|
|
let mechanism_confidence = if rt3_105_post_span_bridge_probe.is_some() {
|
|
"mixed"
|
|
} else {
|
|
"inferred"
|
|
}
|
|
.to_string();
|
|
let mut notes = Vec::new();
|
|
if let Some(bridge) = rt3_105_post_span_bridge_probe {
|
|
notes.push(format!(
|
|
"RT3 1.05 save branch uses {} with selector/descriptor {} -> {}.",
|
|
bridge.bridge_family, bridge.selector_high_hex, bridge.descriptor_high_hex
|
|
));
|
|
} else {
|
|
notes.push(
|
|
"RT3 1.05 save exposes a packed-profile analogue, but the upstream load bridge is not resolved for this branch."
|
|
.to_string(),
|
|
);
|
|
}
|
|
if let Some(map_path) = &block.map_path {
|
|
notes.push(format!("Packed profile map path: {map_path}"));
|
|
}
|
|
if let Some(display_name) = &block.display_name {
|
|
notes.push(format!("Packed profile display name: {display_name}"));
|
|
}
|
|
if let Some(table) = &candidate_table {
|
|
notes.push(format!(
|
|
"Candidate table source {} carries {} entries with {} zero-availability overrides.",
|
|
table.source_kind, table.observed_entry_count, table.zero_availability_count
|
|
));
|
|
}
|
|
|
|
return Some(SmpSaveLoadSummary {
|
|
file_extension_hint,
|
|
container_profile_family,
|
|
mechanism_family,
|
|
mechanism_confidence,
|
|
packed_profile_kind: Some("rt3-105-packed-profile".to_string()),
|
|
packed_profile_family: Some(probe.profile_family.clone()),
|
|
packed_profile_offset: Some(probe.packed_profile_offset),
|
|
packed_profile_len: Some(probe.packed_profile_len),
|
|
map_path: block.map_path.clone(),
|
|
display_name: block.display_name.clone(),
|
|
profile_byte_0x77: Some(block.profile_byte_0x77),
|
|
profile_byte_0x77_hex: Some(block.profile_byte_0x77_hex.clone()),
|
|
profile_byte_0x82: Some(block.profile_byte_0x82),
|
|
profile_byte_0x82_hex: Some(block.profile_byte_0x82_hex.clone()),
|
|
profile_byte_0x97: Some(block.profile_byte_0x97),
|
|
profile_byte_0x97_hex: Some(block.profile_byte_0x97_hex.clone()),
|
|
profile_byte_0xc5: Some(block.profile_byte_0xc5),
|
|
profile_byte_0xc5_hex: Some(block.profile_byte_0xc5_hex.clone()),
|
|
trailer_family,
|
|
bridge_family,
|
|
candidate_table,
|
|
notes,
|
|
});
|
|
}
|
|
|
|
if let Some(table) = candidate_table {
|
|
return Some(SmpSaveLoadSummary {
|
|
file_extension_hint,
|
|
container_profile_family,
|
|
mechanism_family: "rt3-105-candidate-catalog-source-v1".to_string(),
|
|
mechanism_confidence: "mixed".to_string(),
|
|
packed_profile_kind: None,
|
|
packed_profile_family: None,
|
|
packed_profile_offset: None,
|
|
packed_profile_len: None,
|
|
map_path: None,
|
|
display_name: None,
|
|
profile_byte_0x77: None,
|
|
profile_byte_0x77_hex: None,
|
|
profile_byte_0x82: None,
|
|
profile_byte_0x82_hex: None,
|
|
profile_byte_0x97: None,
|
|
profile_byte_0x97_hex: None,
|
|
profile_byte_0xc5: None,
|
|
profile_byte_0xc5_hex: None,
|
|
trailer_family,
|
|
bridge_family,
|
|
notes: vec![
|
|
format!(
|
|
"The file carries the shared 1.05 candidate table source block through {}.",
|
|
table.source_kind
|
|
),
|
|
format!(
|
|
"The table exposes {} named entries with {} zero-availability overrides.",
|
|
table.observed_entry_count, table.zero_availability_count
|
|
),
|
|
],
|
|
candidate_table: Some(table),
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn parse_preamble(bytes: &[u8]) -> SmpPreamble {
|
|
let byte_len = bytes.len().min(PREAMBLE_U32_WORD_COUNT * 4);
|
|
let words = bytes[..byte_len]
|
|
.chunks_exact(4)
|
|
.enumerate()
|
|
.map(|(index, chunk)| {
|
|
let value_le = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
|
SmpPreambleWord {
|
|
index,
|
|
offset: index * 4,
|
|
value_le,
|
|
value_hex: format!("0x{value_le:08x}"),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
SmpPreamble {
|
|
byte_len,
|
|
word_count: words.len(),
|
|
words,
|
|
}
|
|
}
|
|
|
|
fn parse_shared_header(bytes: &[u8]) -> Option<SmpSharedHeader> {
|
|
let words = read_preamble_words(bytes)?;
|
|
let shared_signature_words_1_to_7 = words[1..=7].to_vec();
|
|
let payload_window_words_8_to_9 = words[8..=9].to_vec();
|
|
let reserved_words_10_to_14 = words[10..=14].to_vec();
|
|
let final_flag_word = words[15];
|
|
|
|
Some(SmpSharedHeader {
|
|
byte_len: PREAMBLE_U32_WORD_COUNT * 4,
|
|
root_kind_word: words[0],
|
|
root_kind_word_hex: format!("0x{:08x}", words[0]),
|
|
primary_family_tag: words[1],
|
|
primary_family_tag_hex: format!("0x{:08x}", words[1]),
|
|
shared_signature_hex_words_1_to_7: shared_signature_words_1_to_7
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
matches_grounded_common_signature: shared_signature_words_1_to_7
|
|
== SHARED_SIGNATURE_WORDS_1_TO_7,
|
|
shared_signature_words_1_to_7,
|
|
payload_window_hex_words_8_to_9: payload_window_words_8_to_9
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
payload_window_words_8_to_9,
|
|
reserved_words_10_to_14_all_zero: reserved_words_10_to_14.iter().all(|word| *word == 0),
|
|
reserved_words_10_to_14,
|
|
final_flag_word,
|
|
final_flag_word_hex: format!("0x{final_flag_word:08x}"),
|
|
})
|
|
}
|
|
|
|
fn classify_header_variant_probe(shared_header: &SmpSharedHeader) -> SmpHeaderVariantProbe {
|
|
let words = &shared_header.shared_signature_words_1_to_7;
|
|
let root = shared_header.root_kind_word;
|
|
let final_flag = shared_header.final_flag_word;
|
|
|
|
let (variant_family, evidence, is_known_family) = match (root, words.as_slice(), final_flag) {
|
|
(
|
|
0x00002649,
|
|
[
|
|
0x00002ee0,
|
|
0x00040001,
|
|
0x00028000,
|
|
0x00010000,
|
|
0x00000771,
|
|
0x00000771,
|
|
0x00000771,
|
|
],
|
|
0x00000001,
|
|
) => (
|
|
"rt3-105-gmx-header-v1".to_string(),
|
|
vec![
|
|
"root kind word 0x00002649".to_string(),
|
|
"1.05 common signature words 1..7".to_string(),
|
|
"final flag 0x00000001".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
(
|
|
0x000025e5,
|
|
[
|
|
0x00002ee0,
|
|
0x00040001,
|
|
0x00028000,
|
|
0x00010000,
|
|
0x00000771,
|
|
0x00000771,
|
|
0x00000771,
|
|
],
|
|
0x00000000,
|
|
) => (
|
|
"rt3-105-common-header-v1".to_string(),
|
|
vec![
|
|
"root kind word 0x000025e5".to_string(),
|
|
"1.05 common signature words 1..7".to_string(),
|
|
"final flag 0x00000000".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
(
|
|
0x000025e5,
|
|
[
|
|
0x00002ee0,
|
|
0x00040001,
|
|
0x00018000,
|
|
0x00010000,
|
|
0x00000746,
|
|
0x00000746,
|
|
0x00000746,
|
|
],
|
|
0x00000000,
|
|
) => (
|
|
"rt3-105-scenario-save-header-v1".to_string(),
|
|
vec![
|
|
"root kind word 0x000025e5".to_string(),
|
|
"1.05 scenario-save signature words 1..7".to_string(),
|
|
"final flag 0x00000000".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
(
|
|
0x000025e5,
|
|
[
|
|
0x00002ee0,
|
|
0x0001c001,
|
|
0x00018000,
|
|
0x00010000,
|
|
0x00000754,
|
|
0x00000754,
|
|
0x00000754,
|
|
],
|
|
0x00000000,
|
|
) => (
|
|
"rt3-105-alt-save-header-v1".to_string(),
|
|
vec![
|
|
"root kind word 0x000025e5".to_string(),
|
|
"1.05 alternate-save signature words 1..7".to_string(),
|
|
"final flag 0x00000000".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
(
|
|
0x000026ad,
|
|
[
|
|
0x00002ee0,
|
|
0x00014001,
|
|
0x00020000,
|
|
0x00010000,
|
|
0x00000725,
|
|
0x00000725,
|
|
0x00000725,
|
|
],
|
|
0x00000100,
|
|
) => (
|
|
"rt3-classic-gms-header-v1".to_string(),
|
|
vec![
|
|
"root kind word 0x000026ad".to_string(),
|
|
"classic save signature words 1..7".to_string(),
|
|
"final flag 0x00000100".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
(
|
|
0x000026ad,
|
|
[
|
|
0x00002ee0,
|
|
0x0001c001,
|
|
0x00018000,
|
|
0x00010000,
|
|
0x00000765,
|
|
0x00000765,
|
|
0x00000765,
|
|
],
|
|
0x00000001,
|
|
) => (
|
|
"rt3-classic-gmx-header-v1".to_string(),
|
|
vec![
|
|
"root kind word 0x000026ad".to_string(),
|
|
"classic sandbox signature words 1..7".to_string(),
|
|
"final flag 0x00000001".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
(0x000025e5, [0x00002ee0, _, _, 0x00010000, _, _, _], 0x00000000 | 0x00000100) => (
|
|
"rt3-map-header-family".to_string(),
|
|
vec![
|
|
"root kind word 0x000025e5".to_string(),
|
|
"map-family anchor 0x00002ee0".to_string(),
|
|
"word4 0x00010000".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
_ => (
|
|
"unknown".to_string(),
|
|
vec![format!(
|
|
"root=0x{root:08x}, words1..7={}, final=0x{final_flag:08x}",
|
|
words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
)],
|
|
false,
|
|
),
|
|
};
|
|
|
|
SmpHeaderVariantProbe {
|
|
variant_family,
|
|
variant_evidence: evidence,
|
|
is_known_family,
|
|
}
|
|
}
|
|
|
|
fn read_preamble_words(bytes: &[u8]) -> Option<[u32; PREAMBLE_U32_WORD_COUNT]> {
|
|
if bytes.len() < PREAMBLE_U32_WORD_COUNT * 4 {
|
|
return None;
|
|
}
|
|
|
|
let mut words = [0u32; PREAMBLE_U32_WORD_COUNT];
|
|
for (index, chunk) in bytes[..PREAMBLE_U32_WORD_COUNT * 4]
|
|
.chunks_exact(4)
|
|
.enumerate()
|
|
{
|
|
words[index] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
|
}
|
|
Some(words)
|
|
}
|
|
|
|
fn probe_early_content_layout(
|
|
bytes: &[u8],
|
|
ascii_run: &SmpAsciiPreview,
|
|
) -> Option<SmpEarlyContentProbe> {
|
|
let search_start = ascii_run.offset + ascii_run.byte_len;
|
|
let first_post_text_nonzero_offset = find_next_nonzero_offset(bytes, search_start)?;
|
|
let zero_pad_after_text_len = first_post_text_nonzero_offset.saturating_sub(search_start);
|
|
let first_zero_run_after_block = find_zero_run(
|
|
bytes,
|
|
first_post_text_nonzero_offset,
|
|
EARLY_ZERO_RUN_THRESHOLD,
|
|
)
|
|
.unwrap_or(bytes.len());
|
|
let first_post_text_block = &bytes[first_post_text_nonzero_offset..first_zero_run_after_block];
|
|
let secondary_nonzero_offset = find_next_nonzero_offset(bytes, first_zero_run_after_block);
|
|
let trailing_zero_pad_after_first_block_len = secondary_nonzero_offset
|
|
.map(|offset| offset.saturating_sub(first_zero_run_after_block))
|
|
.unwrap_or_else(|| bytes.len().saturating_sub(first_zero_run_after_block));
|
|
let secondary_aligned_word_window_offset = secondary_nonzero_offset.map(|offset| offset & !0x3);
|
|
let secondary_aligned_word_window_words = secondary_aligned_word_window_offset
|
|
.map(|offset| read_u32_window(bytes, offset, EARLY_ALIGNED_WORD_WINDOW_COUNT))
|
|
.unwrap_or_default();
|
|
let secondary_preview_hex = secondary_nonzero_offset
|
|
.map(|offset| {
|
|
hex_encode(&bytes[offset..bytes.len().min(offset + EARLY_PREVIEW_BYTE_LIMIT)])
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
Some(SmpEarlyContentProbe {
|
|
first_post_text_nonzero_offset,
|
|
zero_pad_after_text_len,
|
|
first_post_text_block_len: first_post_text_block.len(),
|
|
first_post_text_block_hex: hex_encode(first_post_text_block),
|
|
trailing_zero_pad_after_first_block_len,
|
|
secondary_nonzero_offset,
|
|
secondary_aligned_word_window_offset,
|
|
secondary_aligned_word_window_hex_words: secondary_aligned_word_window_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
secondary_aligned_word_window_words,
|
|
secondary_preview_hex,
|
|
})
|
|
}
|
|
|
|
fn classify_secondary_variant_probe(
|
|
probe: &SmpEarlyContentProbe,
|
|
) -> Option<SmpSecondaryVariantProbe> {
|
|
let aligned_window_offset = probe.secondary_aligned_word_window_offset?;
|
|
let words = probe.secondary_aligned_word_window_words.clone();
|
|
if words.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let mut evidence = Vec::new();
|
|
let variant_family = match words.as_slice() {
|
|
[0x001e0000, 0x86a00100, 0x03000001, 0xf0000100, ..] => {
|
|
evidence.push("leading word 0x001e0000".to_string());
|
|
evidence.push("anchor word 0x86a00100".to_string());
|
|
evidence.push("third/fourth words 0x03000001 and 0xf0000100".to_string());
|
|
"rt3-gms-family-v1".to_string()
|
|
}
|
|
[0x000a0000, 0x49f00100, 0x00000002, 0xa0000000, ..] => {
|
|
evidence.push("leading word 0x000a0000".to_string());
|
|
evidence.push("anchor word 0x49f00100".to_string());
|
|
evidence.push("third/fourth words 0x00000002 and 0xa0000000".to_string());
|
|
"rt3-gmx-family-v1".to_string()
|
|
}
|
|
[0x001c0000, 0x86a00100, 0x00000001, 0xa0000000, ..] => {
|
|
evidence.push("leading word 0x001c0000".to_string());
|
|
evidence.push("anchor word 0x86a00100".to_string());
|
|
evidence.push("third/fourth words 0x00000001 and 0xa0000000".to_string());
|
|
"rt3-105-gms-family-v1".to_string()
|
|
}
|
|
[0x00190000, 0x86a00100, 0x00000001, 0xa0000000, ..] => {
|
|
evidence.push("leading word 0x00190000".to_string());
|
|
evidence.push("anchor word 0x86a00100".to_string());
|
|
evidence.push("third/fourth words 0x00000001 and 0xa0000000".to_string());
|
|
"rt3-105-gmx-family-v1".to_string()
|
|
}
|
|
[0x00130000, 0x86a00100, 0x21000001, 0xa0000100, ..] => {
|
|
evidence.push("leading word 0x00130000".to_string());
|
|
evidence.push("anchor word 0x86a00100".to_string());
|
|
evidence.push("third/fourth words 0x21000001 and 0xa0000100".to_string());
|
|
"rt3-105-gms-scenario-family-v1".to_string()
|
|
}
|
|
[0x00010000, 0x49f00100, 0x00000002, 0xa0000000, ..] => {
|
|
evidence.push("leading word 0x00010000".to_string());
|
|
evidence.push("anchor word 0x49f00100".to_string());
|
|
evidence.push("third/fourth words 0x00000002 and 0xa0000000".to_string());
|
|
"rt3-105-gms-alt-family-v1".to_string()
|
|
}
|
|
[0x86a00100, 0x00000001, 0xa0000000, 0x00000186, ..] => {
|
|
evidence.push("window starts directly on 0x86a00100".to_string());
|
|
evidence.push("likely same family with missing leading unaligned word".to_string());
|
|
"rt3-family-unaligned-anchor".to_string()
|
|
}
|
|
_ => {
|
|
evidence.push(format!(
|
|
"unrecognized leading words: {}",
|
|
words
|
|
.iter()
|
|
.take(4)
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
));
|
|
"unknown".to_string()
|
|
}
|
|
};
|
|
|
|
Some(SmpSecondaryVariantProbe {
|
|
aligned_window_offset,
|
|
hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(),
|
|
words,
|
|
variant_family,
|
|
variant_evidence: evidence,
|
|
})
|
|
}
|
|
|
|
fn classify_container_profile(
|
|
file_extension_hint: Option<&str>,
|
|
header_variant_probe: Option<&SmpHeaderVariantProbe>,
|
|
secondary_variant_probe: Option<&SmpSecondaryVariantProbe>,
|
|
) -> Option<SmpContainerProfile> {
|
|
let header_family = header_variant_probe.map(|probe| probe.variant_family.as_str())?;
|
|
let secondary_family = secondary_variant_probe.map(|probe| probe.variant_family.as_str())?;
|
|
let extension = file_extension_hint.unwrap_or("");
|
|
|
|
let (profile_family, profile_evidence, is_known_profile) =
|
|
match (extension, header_family, secondary_family) {
|
|
("gms", "rt3-classic-gms-header-v1", "rt3-gms-family-v1") => (
|
|
"rt3-classic-save-container-v1".to_string(),
|
|
vec![
|
|
"extension .gms".to_string(),
|
|
"classic save header family".to_string(),
|
|
"classic save secondary window family".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gmx", "rt3-classic-gmx-header-v1", "rt3-gmx-family-v1") => (
|
|
"rt3-classic-sandbox-container-v1".to_string(),
|
|
vec![
|
|
"extension .gmx".to_string(),
|
|
"classic sandbox header family".to_string(),
|
|
"classic sandbox secondary window family".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gms", "rt3-105-common-header-v1", "rt3-105-gms-family-v1") => (
|
|
"rt3-105-save-container-v1".to_string(),
|
|
vec![
|
|
"extension .gms".to_string(),
|
|
"1.05 common header family".to_string(),
|
|
"1.05 save secondary window family".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gms", "rt3-105-scenario-save-header-v1", "rt3-105-gms-scenario-family-v1") => (
|
|
"rt3-105-scenario-save-container-v1".to_string(),
|
|
vec![
|
|
"extension .gms".to_string(),
|
|
"1.05 scenario-save header family".to_string(),
|
|
"1.05 scenario-save secondary window family".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gms", "rt3-105-alt-save-header-v1", "rt3-105-gms-alt-family-v1") => (
|
|
"rt3-105-alt-save-container-v1".to_string(),
|
|
vec![
|
|
"extension .gms".to_string(),
|
|
"1.05 alternate-save header family".to_string(),
|
|
"1.05 alternate-save secondary window family".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gmx", "rt3-105-gmx-header-v1", "rt3-105-gmx-family-v1") => (
|
|
"rt3-105-sandbox-container-v1".to_string(),
|
|
vec![
|
|
"extension .gmx".to_string(),
|
|
"1.05 sandbox header family".to_string(),
|
|
"1.05 sandbox secondary window family".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gmp", "rt3-105-common-header-v1", "rt3-family-unaligned-anchor") => (
|
|
"rt3-105-map-container-v1".to_string(),
|
|
vec![
|
|
"extension .gmp".to_string(),
|
|
"1.05 common header family".to_string(),
|
|
"map-style secondary unaligned anchor".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gmp", "rt3-105-scenario-save-header-v1", "unknown") => (
|
|
"rt3-105-scenario-map-container-v1".to_string(),
|
|
vec![
|
|
"extension .gmp".to_string(),
|
|
"1.05 scenario-map header family".to_string(),
|
|
"fixed candidate-availability table range present despite unknown early secondary window".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gmp", "rt3-105-alt-save-header-v1", "unknown") => (
|
|
"rt3-105-alt-map-container-v1".to_string(),
|
|
vec![
|
|
"extension .gmp".to_string(),
|
|
"1.05 alternate-map header family".to_string(),
|
|
"fixed candidate-availability table range present despite unknown early secondary window".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
("gmp", "rt3-map-header-family", "rt3-family-unaligned-anchor") => (
|
|
"rt3-map-container-family".to_string(),
|
|
vec![
|
|
"extension .gmp".to_string(),
|
|
"map header family".to_string(),
|
|
"map-style secondary unaligned anchor".to_string(),
|
|
],
|
|
true,
|
|
),
|
|
(_, header_family, secondary_family) => (
|
|
"unknown".to_string(),
|
|
vec![
|
|
format!(
|
|
"extension {}",
|
|
if extension.is_empty() {
|
|
"<none>"
|
|
} else {
|
|
extension
|
|
}
|
|
),
|
|
format!("header family {header_family}"),
|
|
format!("secondary family {secondary_family}"),
|
|
],
|
|
false,
|
|
),
|
|
};
|
|
|
|
Some(SmpContainerProfile {
|
|
profile_family,
|
|
profile_evidence,
|
|
is_known_profile,
|
|
})
|
|
}
|
|
|
|
fn parse_save_bootstrap_block(
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
secondary_variant_probe: Option<&SmpSecondaryVariantProbe>,
|
|
) -> Option<SmpSaveBootstrapBlock> {
|
|
let profile = container_profile?;
|
|
let secondary = secondary_variant_probe?;
|
|
let words = &secondary.words;
|
|
if words.len() < 8 {
|
|
return None;
|
|
}
|
|
|
|
let supported = matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
);
|
|
if !supported {
|
|
return None;
|
|
}
|
|
|
|
Some(SmpSaveBootstrapBlock {
|
|
profile_family: profile.profile_family.clone(),
|
|
aligned_window_offset: secondary.aligned_window_offset,
|
|
leading_word: words[0],
|
|
leading_word_hex: format!("0x{:08x}", words[0]),
|
|
anchor_word: words[1],
|
|
anchor_word_hex: format!("0x{:08x}", words[1]),
|
|
descriptor_word_2: words[2],
|
|
descriptor_word_2_hex: format!("0x{:08x}", words[2]),
|
|
descriptor_word_3: words[3],
|
|
descriptor_word_3_hex: format!("0x{:08x}", words[3]),
|
|
descriptor_word_4: words[4],
|
|
descriptor_word_4_hex: format!("0x{:08x}", words[4]),
|
|
descriptor_word_5: words[5],
|
|
descriptor_word_5_hex: format!("0x{:08x}", words[5]),
|
|
descriptor_word_6: words[6],
|
|
descriptor_word_6_hex: format!("0x{:08x}", words[6]),
|
|
descriptor_word_7: words[7],
|
|
descriptor_word_7_hex: format!("0x{:08x}", words[7]),
|
|
})
|
|
}
|
|
|
|
fn parse_runtime_anchor_cycle_block(
|
|
bytes: &[u8],
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
secondary_variant_probe: Option<&SmpSecondaryVariantProbe>,
|
|
) -> Option<SmpRuntimeAnchorCycleBlock> {
|
|
let profile = container_profile?;
|
|
let secondary = secondary_variant_probe?;
|
|
let supported = matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-classic-sandbox-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
| "rt3-105-sandbox-container-v1"
|
|
);
|
|
if !supported {
|
|
return None;
|
|
}
|
|
|
|
let cycle_start_offset = secondary.aligned_window_offset + 0x1c;
|
|
let cycle_words = read_u32_window(bytes, cycle_start_offset, 9);
|
|
if cycle_words.len() < 9 {
|
|
return None;
|
|
}
|
|
|
|
let mut full_cycle_count = 0usize;
|
|
let mut cursor = cycle_start_offset;
|
|
while read_u32_window(bytes, cursor, cycle_words.len()) == cycle_words {
|
|
full_cycle_count += 1;
|
|
cursor += cycle_words.len() * 4;
|
|
}
|
|
|
|
if full_cycle_count == 0 {
|
|
return None;
|
|
}
|
|
|
|
let mut partial_cycle_word_count = 0usize;
|
|
while partial_cycle_word_count < cycle_words.len() {
|
|
let offset = cursor + partial_cycle_word_count * 4;
|
|
if read_u32_at(bytes, offset) == Some(cycle_words[partial_cycle_word_count]) {
|
|
partial_cycle_word_count += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let trailer_offset = cursor + partial_cycle_word_count * 4;
|
|
let trailer_words = read_u32_window(bytes, trailer_offset, 16);
|
|
|
|
Some(SmpRuntimeAnchorCycleBlock {
|
|
profile_family: profile.profile_family.clone(),
|
|
cycle_start_offset,
|
|
cycle_hex_words: cycle_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
cycle_words,
|
|
full_cycle_count,
|
|
partial_cycle_word_count,
|
|
trailer_offset,
|
|
trailer_hex_words: trailer_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
trailer_words,
|
|
})
|
|
}
|
|
|
|
fn parse_save_anchor_run_block(
|
|
bytes: &[u8],
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
save_bootstrap_block: Option<&SmpSaveBootstrapBlock>,
|
|
) -> Option<SmpSaveAnchorRunBlock> {
|
|
let profile = container_profile?;
|
|
let bootstrap = save_bootstrap_block?;
|
|
let supported = matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
);
|
|
if !supported {
|
|
return None;
|
|
}
|
|
|
|
let cycle_start_offset = bootstrap.aligned_window_offset + 0x1c;
|
|
let cycle_words = read_u32_window(bytes, cycle_start_offset, 9);
|
|
if cycle_words.len() < 9 {
|
|
return None;
|
|
}
|
|
|
|
let mut full_cycle_count = 0usize;
|
|
let mut cursor = cycle_start_offset;
|
|
while read_u32_window(bytes, cursor, cycle_words.len()) == cycle_words {
|
|
full_cycle_count += 1;
|
|
cursor += cycle_words.len() * 4;
|
|
}
|
|
|
|
if full_cycle_count == 0 {
|
|
return None;
|
|
}
|
|
|
|
let mut partial_cycle_word_count = 0usize;
|
|
while partial_cycle_word_count < cycle_words.len() {
|
|
let offset = cursor + partial_cycle_word_count * 4;
|
|
if read_u32_at(bytes, offset) == Some(cycle_words[partial_cycle_word_count]) {
|
|
partial_cycle_word_count += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let trailer_offset = cursor + partial_cycle_word_count * 4;
|
|
let trailer_words = read_u32_window(bytes, trailer_offset, 12);
|
|
|
|
Some(SmpSaveAnchorRunBlock {
|
|
profile_family: profile.profile_family.clone(),
|
|
cycle_start_offset,
|
|
cycle_hex_words: cycle_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
cycle_words,
|
|
full_cycle_count,
|
|
partial_cycle_word_count,
|
|
trailer_offset,
|
|
trailer_hex_words: trailer_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
trailer_words,
|
|
})
|
|
}
|
|
|
|
fn parse_runtime_trailer_block(
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
runtime_anchor_cycle_block: Option<&SmpRuntimeAnchorCycleBlock>,
|
|
) -> Option<SmpRuntimeTrailerBlock> {
|
|
let profile = container_profile?;
|
|
let anchor = runtime_anchor_cycle_block?;
|
|
let words = &anchor.trailer_words;
|
|
if words.len() < 16 {
|
|
return None;
|
|
}
|
|
|
|
let trailer_family = match profile.profile_family.as_str() {
|
|
"rt3-classic-save-container-v1"
|
|
if words[..6]
|
|
== [
|
|
0x00020000, 0x00030000, 0x00010000, 0x00010000, 0x00010000, 0x00020000,
|
|
] =>
|
|
{
|
|
"rt3-classic-save-trailer-v1"
|
|
}
|
|
"rt3-classic-sandbox-container-v1"
|
|
if words[..6]
|
|
== [
|
|
0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000,
|
|
] =>
|
|
{
|
|
"rt3-classic-sandbox-trailer-v1"
|
|
}
|
|
"rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
if words[..6]
|
|
== [
|
|
0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000,
|
|
] =>
|
|
{
|
|
"rt3-105-save-trailer-v1"
|
|
}
|
|
"rt3-105-sandbox-container-v1"
|
|
if words[..6]
|
|
== [
|
|
0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000,
|
|
] =>
|
|
{
|
|
"rt3-105-sandbox-trailer-v1"
|
|
}
|
|
_ => "unknown",
|
|
}
|
|
.to_string();
|
|
|
|
let tag_chunk_id_u16 = (words[6] >> 16) as u16;
|
|
let length_high_u16 = (words[7] >> 16) as u16;
|
|
let selector_high_u16 = (words[8] >> 16) as u16;
|
|
let descriptor_high_u16 = (words[10] >> 16) as u16;
|
|
let tag_chunk_id_grounded_alignment =
|
|
classify_runtime_trailer_chunk_id_grounded_alignment(tag_chunk_id_u16).map(str::to_string);
|
|
|
|
let mut trailer_evidence = vec![
|
|
format!("container profile {}", profile.profile_family),
|
|
format!(
|
|
"prefix words {}",
|
|
words[..6]
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
),
|
|
format!("high-16 chunk id 0x{tag_chunk_id_u16:04x} from trailer word 6"),
|
|
format!("high-16 span 0x{length_high_u16:04x} from trailer word 7"),
|
|
format!("high-16 selector 0x{selector_high_u16:04x} from trailer word 8"),
|
|
format!("high-16 descriptor 0x{descriptor_high_u16:04x} from trailer word 10"),
|
|
];
|
|
if let Some(alignment) = &tag_chunk_id_grounded_alignment {
|
|
trailer_evidence.push(alignment.clone());
|
|
}
|
|
|
|
Some(SmpRuntimeTrailerBlock {
|
|
profile_family: profile.profile_family.clone(),
|
|
trailer_family,
|
|
trailer_evidence,
|
|
trailer_offset: anchor.trailer_offset,
|
|
prefix_words_0_to_5: words[..6].to_vec(),
|
|
prefix_hex_words_0_to_5: words[..6]
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
tag_word_6: words[6],
|
|
tag_word_6_hex: format!("0x{:08x}", words[6]),
|
|
tag_chunk_id_u16,
|
|
tag_chunk_id_hex: format!("0x{tag_chunk_id_u16:04x}"),
|
|
tag_chunk_id_grounded_alignment,
|
|
length_word_7: words[7],
|
|
length_word_7_hex: format!("0x{:08x}", words[7]),
|
|
length_high_u16,
|
|
length_high_hex: format!("0x{length_high_u16:04x}"),
|
|
selector_word_8: words[8],
|
|
selector_word_8_hex: format!("0x{:08x}", words[8]),
|
|
selector_high_u16,
|
|
selector_high_hex: format!("0x{selector_high_u16:04x}"),
|
|
layout_word_9: words[9],
|
|
layout_word_9_hex: format!("0x{:08x}", words[9]),
|
|
descriptor_word_10: words[10],
|
|
descriptor_word_10_hex: format!("0x{:08x}", words[10]),
|
|
descriptor_high_u16,
|
|
descriptor_high_hex: format!("0x{descriptor_high_u16:04x}"),
|
|
descriptor_word_11: words[11],
|
|
descriptor_word_11_hex: format!("0x{:08x}", words[11]),
|
|
counter_word_12: words[12],
|
|
counter_word_12_hex: format!("0x{:08x}", words[12]),
|
|
offset_word_13: words[13],
|
|
offset_word_13_hex: format!("0x{:08x}", words[13]),
|
|
span_word_14: words[14],
|
|
span_word_14_hex: format!("0x{:08x}", words[14]),
|
|
mode_word_15: words[15],
|
|
mode_word_15_hex: format!("0x{:08x}", words[15]),
|
|
words: words.to_vec(),
|
|
hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(),
|
|
})
|
|
}
|
|
|
|
fn classify_runtime_trailer_chunk_id_grounded_alignment(
|
|
tag_chunk_id_u16: u16,
|
|
) -> Option<&'static str> {
|
|
match tag_chunk_id_u16 {
|
|
0x2ee1 => Some(
|
|
"High-16 chunk id 0x2ee1 matches the disassembly-grounded map-style bundle family already read by shell_setup_load_selected_profile_bundle_into_payload_record.",
|
|
),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn parse_runtime_post_span_probe(
|
|
bytes: &[u8],
|
|
runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>,
|
|
) -> Option<SmpRuntimePostSpanProbe> {
|
|
let trailer = runtime_trailer_block?;
|
|
let span_target_offset = trailer.trailer_offset + trailer.length_high_u16 as usize;
|
|
let next_nonzero_offset = find_next_nonzero_offset(bytes, span_target_offset);
|
|
let header_candidates =
|
|
collect_runtime_post_span_header_candidates(bytes, span_target_offset, 0x8000);
|
|
let next_aligned_candidate_offset = header_candidates.first().map(|candidate| candidate.offset);
|
|
let next_aligned_candidate_words = header_candidates
|
|
.first()
|
|
.map(|candidate| candidate.words.clone())
|
|
.unwrap_or_default();
|
|
let grounded_progress_hits =
|
|
find_grounded_progress_high16_hits(bytes, span_target_offset, 0x8000);
|
|
|
|
Some(SmpRuntimePostSpanProbe {
|
|
profile_family: trailer.profile_family.clone(),
|
|
span_target_offset,
|
|
next_nonzero_offset,
|
|
next_aligned_candidate_offset,
|
|
next_aligned_candidate_hex_words: next_aligned_candidate_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
next_aligned_candidate_words,
|
|
header_candidates,
|
|
grounded_progress_hits,
|
|
})
|
|
}
|
|
|
|
fn parse_classic_rehydrate_profile_probe(
|
|
bytes: &[u8],
|
|
runtime_post_span_probe: Option<&SmpRuntimePostSpanProbe>,
|
|
) -> Option<SmpClassicRehydrateProfileProbe> {
|
|
let post_span = runtime_post_span_probe?;
|
|
if post_span.profile_family != "rt3-classic-save-container-v1" {
|
|
return None;
|
|
}
|
|
|
|
let progress_32dc_offset =
|
|
parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x32dc)?;
|
|
let progress_3714_offset =
|
|
parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x3714)?;
|
|
let progress_3715_offset =
|
|
parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x3715)?;
|
|
let packed_profile_offset = progress_3714_offset + 4;
|
|
let packed_profile_len = progress_3715_offset.checked_sub(packed_profile_offset)?;
|
|
if packed_profile_len != 0x108 {
|
|
return None;
|
|
}
|
|
|
|
let ascii_runs =
|
|
collect_ascii_previews_in_range(bytes, packed_profile_offset, progress_3715_offset, 4);
|
|
let packed_profile_block =
|
|
parse_classic_packed_profile_block(bytes, packed_profile_offset, packed_profile_len)?;
|
|
|
|
Some(SmpClassicRehydrateProfileProbe {
|
|
profile_family: post_span.profile_family.clone(),
|
|
progress_32dc_offset,
|
|
progress_3714_offset,
|
|
progress_3715_offset,
|
|
packed_profile_offset,
|
|
packed_profile_len,
|
|
packed_profile_len_hex: format!("0x{packed_profile_len:03x}"),
|
|
packed_profile_block,
|
|
ascii_runs,
|
|
})
|
|
}
|
|
|
|
fn parse_classic_packed_profile_block(
|
|
bytes: &[u8],
|
|
packed_profile_offset: usize,
|
|
packed_profile_len: usize,
|
|
) -> Option<SmpClassicPackedProfileBlock> {
|
|
let block_end = packed_profile_offset.checked_add(packed_profile_len)?;
|
|
if block_end > bytes.len() || packed_profile_len != 0x108 {
|
|
return None;
|
|
}
|
|
|
|
let leading_word_0 = read_u32_at(bytes, packed_profile_offset)?;
|
|
let trailing_zero_word_count_after_leading_word = (1..4)
|
|
.take_while(|index| {
|
|
read_u32_at(bytes, packed_profile_offset + (index * 4)).is_some_and(|word| word == 0)
|
|
})
|
|
.count();
|
|
let map_path_offset = 0x13;
|
|
let display_name_offset = 0x46;
|
|
let stable_nonzero_word_offsets = [0x00usize, 0x10, 0x78, 0x7c, 0x84, 0x88];
|
|
let stable_nonzero_words = stable_nonzero_word_offsets
|
|
.iter()
|
|
.filter_map(|relative_offset| {
|
|
let value = read_u32_at(bytes, packed_profile_offset + relative_offset)?;
|
|
if value == 0 {
|
|
return None;
|
|
}
|
|
|
|
Some(SmpPackedProfileWordLane {
|
|
relative_offset: *relative_offset,
|
|
relative_offset_hex: format!("0x{relative_offset:02x}"),
|
|
value,
|
|
value_hex: format!("0x{value:08x}"),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
Some(SmpClassicPackedProfileBlock {
|
|
relative_len: packed_profile_len,
|
|
relative_len_hex: format!("0x{packed_profile_len:03x}"),
|
|
leading_word_0,
|
|
leading_word_0_hex: format!("0x{leading_word_0:08x}"),
|
|
trailing_zero_word_count_after_leading_word,
|
|
map_path_offset,
|
|
map_path: read_c_string_in_range(bytes, packed_profile_offset + map_path_offset, block_end),
|
|
display_name_offset,
|
|
display_name: read_c_string_in_range(
|
|
bytes,
|
|
packed_profile_offset + display_name_offset,
|
|
block_end,
|
|
),
|
|
profile_byte_0x77: bytes[packed_profile_offset + 0x77],
|
|
profile_byte_0x77_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x77]),
|
|
profile_byte_0x82: bytes[packed_profile_offset + 0x82],
|
|
profile_byte_0x82_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x82]),
|
|
profile_byte_0x97: bytes[packed_profile_offset + 0x97],
|
|
profile_byte_0x97_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x97]),
|
|
profile_byte_0xc5: bytes[packed_profile_offset + 0xc5],
|
|
profile_byte_0xc5_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0xc5]),
|
|
stable_nonzero_words,
|
|
})
|
|
}
|
|
|
|
fn parse_rt3_105_packed_profile_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
header_variant_probe: Option<&SmpHeaderVariantProbe>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpRt3105PackedProfileProbe> {
|
|
let profile_family = if container_profile.is_some_and(|profile| {
|
|
matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
)
|
|
}) {
|
|
container_profile
|
|
.expect("checked above")
|
|
.profile_family
|
|
.clone()
|
|
} else if file_extension_hint == Some("gms")
|
|
&& header_variant_probe.is_some_and(|probe| {
|
|
matches!(
|
|
probe.variant_family.as_str(),
|
|
"rt3-105-common-header-v1"
|
|
| "rt3-105-scenario-save-header-v1"
|
|
| "rt3-105-alt-save-header-v1"
|
|
| "rt3-map-header-family"
|
|
)
|
|
})
|
|
{
|
|
"rt3-105-save-analog-block-inferred".to_string()
|
|
} else {
|
|
return None;
|
|
};
|
|
|
|
if file_extension_hint != Some("gms") {
|
|
return None;
|
|
}
|
|
|
|
let map_path_offset = find_c_string_with_suffix_in_range(bytes, 0x7000, 0x9000, ".gmp")?;
|
|
let packed_profile_offset = map_path_offset.checked_sub(0x10)?;
|
|
let packed_profile_len = 0x108usize;
|
|
let block_end = packed_profile_offset.checked_add(packed_profile_len)?;
|
|
if block_end > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let packed_profile_block =
|
|
parse_rt3_105_packed_profile_block(bytes, packed_profile_offset, packed_profile_len)?;
|
|
let ascii_runs = collect_ascii_previews_in_range(bytes, packed_profile_offset, block_end, 4);
|
|
|
|
Some(SmpRt3105PackedProfileProbe {
|
|
profile_family,
|
|
packed_profile_offset,
|
|
packed_profile_len,
|
|
packed_profile_len_hex: format!("0x{packed_profile_len:03x}"),
|
|
packed_profile_block,
|
|
ascii_runs,
|
|
})
|
|
}
|
|
|
|
fn parse_rt3_105_post_span_bridge_probe(
|
|
runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>,
|
|
runtime_post_span_probe: Option<&SmpRuntimePostSpanProbe>,
|
|
rt3_105_packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>,
|
|
) -> Option<SmpRt3105PostSpanBridgeProbe> {
|
|
let trailer = runtime_trailer_block?;
|
|
let post_span = runtime_post_span_probe?;
|
|
let packed_profile = rt3_105_packed_profile_probe?;
|
|
let supported = matches!(
|
|
trailer.profile_family.as_str(),
|
|
"rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
| "rt3-105-save-analog-block-inferred"
|
|
);
|
|
if !supported || trailer.profile_family != post_span.profile_family {
|
|
return None;
|
|
}
|
|
|
|
let next_candidate_high_u16_words = post_span
|
|
.header_candidates
|
|
.first()
|
|
.map(|candidate| candidate.high_u16_words.clone())
|
|
.unwrap_or_default();
|
|
let next_candidate_high_hex_words = next_candidate_high_u16_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:04x}"))
|
|
.collect::<Vec<_>>();
|
|
let next_candidate_offset = post_span.next_aligned_candidate_offset;
|
|
let next_candidate_delta_from_span_target =
|
|
next_candidate_offset.and_then(|offset| offset.checked_sub(post_span.span_target_offset));
|
|
let packed_profile_delta_from_span_target = packed_profile
|
|
.packed_profile_offset
|
|
.checked_sub(post_span.span_target_offset)?;
|
|
let next_candidate_delta_from_packed_profile = next_candidate_offset
|
|
.map(|offset| offset as i64 - packed_profile.packed_profile_offset as i64);
|
|
|
|
let mut bridge_evidence = vec![
|
|
format!("profile family {}", trailer.profile_family),
|
|
format!("selector high {}", trailer.selector_high_hex),
|
|
format!("descriptor high {}", trailer.descriptor_high_hex),
|
|
format!(
|
|
"packed profile sits +0x{packed_profile_delta_from_span_target:x} from span target"
|
|
),
|
|
];
|
|
if let Some(delta) = next_candidate_delta_from_span_target {
|
|
bridge_evidence.push(format!("next candidate sits +0x{delta:x} from span target"));
|
|
}
|
|
if let Some(delta) = next_candidate_delta_from_packed_profile {
|
|
bridge_evidence.push(format!(
|
|
"next candidate is {delta:+#x} relative to packed profile"
|
|
));
|
|
}
|
|
|
|
let bridge_family = match (
|
|
trailer.selector_high_u16,
|
|
trailer.descriptor_high_u16,
|
|
next_candidate_high_u16_words.as_slice(),
|
|
) {
|
|
(0x7110, 0x7801 | 0x7401, [0x6200, 0x0000, 0xfff7, 0x5515, ..]) => {
|
|
bridge_evidence.push(format!(
|
|
"selector/descriptor pair 0x7110 -> 0x{:04x}",
|
|
trailer.descriptor_high_u16
|
|
));
|
|
bridge_evidence.push(
|
|
"next candidate begins with high-16 lanes 0x6200/0x0000/0xfff7/0x5515"
|
|
.to_string(),
|
|
);
|
|
"rt3-105-save-post-span-bridge-v1"
|
|
}
|
|
(0x54cd, 0x5901, [0x1500, 0x0100, 0x4100, 0x0200, ..]) => {
|
|
bridge_evidence.push("selector/descriptor pair 0x54cd -> 0x5901".to_string());
|
|
bridge_evidence.push(
|
|
"next candidate begins with high-16 lanes 0x1500/0x0100/0x4100/0x0200"
|
|
.to_string(),
|
|
);
|
|
"rt3-105-alt-save-post-span-bridge-v1"
|
|
}
|
|
(0x0001, 0x0186, [0x0186, 0x0006, 0x0006, 0x0001, ..]) => {
|
|
bridge_evidence.push("selector/descriptor pair 0x0001 -> 0x0186".to_string());
|
|
bridge_evidence.push(
|
|
"next candidate remains in the local cycle neighborhood with 0x0186/0x0006/0x0006/0x0001"
|
|
.to_string(),
|
|
);
|
|
"rt3-105-scenario-post-span-bridge-v1"
|
|
}
|
|
_ => "unknown",
|
|
}
|
|
.to_string();
|
|
|
|
Some(SmpRt3105PostSpanBridgeProbe {
|
|
profile_family: trailer.profile_family.clone(),
|
|
bridge_family,
|
|
bridge_evidence,
|
|
span_target_offset: post_span.span_target_offset,
|
|
next_candidate_offset,
|
|
next_candidate_delta_from_span_target,
|
|
packed_profile_offset: packed_profile.packed_profile_offset,
|
|
packed_profile_delta_from_span_target,
|
|
next_candidate_delta_from_packed_profile,
|
|
selector_high_u16: trailer.selector_high_u16,
|
|
selector_high_hex: trailer.selector_high_hex.clone(),
|
|
descriptor_high_u16: trailer.descriptor_high_u16,
|
|
descriptor_high_hex: trailer.descriptor_high_hex.clone(),
|
|
next_candidate_high_u16_words,
|
|
next_candidate_high_hex_words,
|
|
})
|
|
}
|
|
|
|
fn parse_rt3_105_save_bridge_payload_probe(
|
|
bytes: &[u8],
|
|
bridge_probe: Option<&SmpRt3105PostSpanBridgeProbe>,
|
|
) -> Option<SmpRt3105SaveBridgePayloadProbe> {
|
|
let bridge = bridge_probe?;
|
|
if bridge.bridge_family != "rt3-105-save-post-span-bridge-v1" {
|
|
return None;
|
|
}
|
|
|
|
let primary_block_offset = bridge.next_candidate_offset?;
|
|
let primary_block_word_count = 8usize;
|
|
let primary_words = read_u32_window(bytes, primary_block_offset, primary_block_word_count);
|
|
if primary_words.len() < primary_block_word_count {
|
|
return None;
|
|
}
|
|
|
|
let secondary_block_delta_from_primary = 0x1808usize;
|
|
let secondary_block_offset = primary_block_offset + secondary_block_delta_from_primary;
|
|
let secondary_block_end_offset = bridge.packed_profile_offset;
|
|
let secondary_block_len = secondary_block_end_offset.checked_sub(secondary_block_offset)?;
|
|
let secondary_preview_word_count = 32usize;
|
|
let secondary_words =
|
|
read_u32_window(bytes, secondary_block_offset, secondary_preview_word_count);
|
|
if secondary_words.len() < secondary_preview_word_count {
|
|
return None;
|
|
}
|
|
|
|
let primary_signature_matches = primary_words
|
|
== [
|
|
0x62000000, 0x00000000, 0xfff70000, 0x55150000, 0x55550000, 0x00000000, 0xfff70000,
|
|
0x54550000,
|
|
];
|
|
let secondary_prefix_matches = secondary_words.starts_with(&[
|
|
0x00050000, 0x00050005, 0xfff70000, 0x54540000, 0x545400f9, 0x00f900f9, 0x00f94008,
|
|
0x00001555,
|
|
]);
|
|
|
|
let mut evidence = vec![
|
|
"bridge family rt3-105-save-post-span-bridge-v1".to_string(),
|
|
format!("primary block offset 0x{primary_block_offset:08x}"),
|
|
format!("secondary block offset 0x{secondary_block_offset:08x}"),
|
|
format!("secondary block delta from primary 0x{secondary_block_delta_from_primary:x}"),
|
|
format!("secondary block end offset 0x{secondary_block_end_offset:08x}"),
|
|
format!("secondary block span 0x{secondary_block_len:x} bytes"),
|
|
];
|
|
if primary_signature_matches {
|
|
evidence.push(
|
|
"primary 8-word bridge block matches the observed 0x6200/0xfff7/0x5515/0x5555 spine"
|
|
.to_string(),
|
|
);
|
|
}
|
|
if secondary_prefix_matches {
|
|
evidence.push(
|
|
"secondary preview matches the observed 0x0005/0xfff7/0x5454 dense block prefix"
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
Some(SmpRt3105SaveBridgePayloadProbe {
|
|
profile_family: bridge.profile_family.clone(),
|
|
bridge_family: bridge.bridge_family.clone(),
|
|
primary_block_offset,
|
|
primary_block_len: primary_block_word_count * 4,
|
|
primary_block_len_hex: format!("0x{:02x}", primary_block_word_count * 4),
|
|
primary_hex_words: primary_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
primary_words,
|
|
secondary_block_offset,
|
|
secondary_block_delta_from_primary,
|
|
secondary_block_delta_from_primary_hex: format!("0x{secondary_block_delta_from_primary:x}"),
|
|
secondary_block_end_offset,
|
|
secondary_block_len,
|
|
secondary_block_len_hex: format!("0x{secondary_block_len:x}"),
|
|
secondary_preview_word_count,
|
|
secondary_hex_words: secondary_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
secondary_words,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn parse_save_world_selection_context_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveWorldSelectionContextProbe> {
|
|
if file_extension_hint != Some("gms") {
|
|
return None;
|
|
}
|
|
let profile = container_profile?;
|
|
let supported = matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
);
|
|
if !supported {
|
|
return None;
|
|
}
|
|
|
|
for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) {
|
|
let payload_offset = chunk_tag_offset + 4;
|
|
let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?;
|
|
if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) {
|
|
continue;
|
|
}
|
|
let selected_company_id_offset =
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET;
|
|
let selected_chairman_profile_id_offset =
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET;
|
|
let chairman_slot_selector_offset =
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET;
|
|
let campaign_override_flag_offset =
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET;
|
|
let chairman_role_gate_offset =
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET;
|
|
let selected_company_id = read_u32_at(bytes, selected_company_id_offset)?;
|
|
let selected_chairman_profile_id = read_u32_at(bytes, selected_chairman_profile_id_offset)?;
|
|
let chairman_slot_selectors = bytes
|
|
.get(
|
|
chairman_slot_selector_offset
|
|
..chairman_slot_selector_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT,
|
|
)?
|
|
.to_vec();
|
|
let campaign_override_flag = *bytes.get(campaign_override_flag_offset)?;
|
|
let chairman_role_gate_bytes = (0..RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT)
|
|
.map(|slot_index| {
|
|
bytes
|
|
.get(
|
|
chairman_role_gate_offset
|
|
+ slot_index * RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE,
|
|
)
|
|
.copied()
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
return Some(SmpSaveWorldSelectionContextProbe {
|
|
profile_family: profile.profile_family.clone(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-selected-company-and-chairman-context".to_string(),
|
|
chunk_tag_offset,
|
|
payload_offset,
|
|
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
|
|
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
|
|
selected_company_id_offset,
|
|
selected_company_id,
|
|
selected_company_id_hex: format!("0x{selected_company_id:08x}"),
|
|
selected_chairman_profile_id_offset,
|
|
selected_chairman_profile_id,
|
|
selected_chairman_profile_id_hex: format!("0x{selected_chairman_profile_id:08x}"),
|
|
chairman_slot_selector_offset,
|
|
chairman_slot_selectors,
|
|
campaign_override_flag_offset,
|
|
campaign_override_flag,
|
|
campaign_override_flag_hex: format!("0x{campaign_override_flag:02x}"),
|
|
chairman_role_gate_offset,
|
|
chairman_role_gate_bytes,
|
|
evidence: vec![
|
|
format!(
|
|
"chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block"
|
|
),
|
|
format!(
|
|
"next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span"
|
|
),
|
|
format!(
|
|
"selected company id comes from payload +0x{:x} ([world+0x21])",
|
|
RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"selected chairman profile id comes from payload +0x{:x} ([world+0x25])",
|
|
RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"16 chairman slot selector bytes come from payload +0x{:x} ([world+0x87])",
|
|
RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"campaign override flag comes from payload +0x{:x} ([world+0xc5])",
|
|
RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"chairman role-gate bytes come from payload +0x{:x} + slot*0x{:x} ([world+0x0bc3+slot*9])",
|
|
RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET,
|
|
RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE
|
|
),
|
|
],
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn parse_save_world_issue_37_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveWorldIssue37Probe> {
|
|
if file_extension_hint != Some("gms") {
|
|
return None;
|
|
}
|
|
let profile = container_profile?;
|
|
let supported = matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
);
|
|
if !supported {
|
|
return None;
|
|
}
|
|
|
|
for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) {
|
|
let payload_offset = chunk_tag_offset + 4;
|
|
let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?;
|
|
if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) {
|
|
continue;
|
|
}
|
|
let issue_value_lane = build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
"issue_0x37_value",
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET,
|
|
)?;
|
|
let issue_37_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET,
|
|
)?;
|
|
let issue_38_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 1,
|
|
)?;
|
|
let issue_39_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 2,
|
|
)?;
|
|
let issue_3a_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 3,
|
|
)?;
|
|
let multiplier_lane = build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
"issue_0x37_multiplier",
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET,
|
|
)?;
|
|
let issue_opinion_base_terms_raw_i32 = build_save_i32_term_strip(
|
|
bytes,
|
|
payload_offset,
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_BASE_TERMS_OFFSET,
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT,
|
|
)?;
|
|
return Some(SmpSaveWorldIssue37Probe {
|
|
profile_family: profile.profile_family.clone(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-save-world-issue-0x37".to_string(),
|
|
chunk_tag_offset,
|
|
payload_offset,
|
|
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
|
|
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
|
|
issue_37_raw_u8,
|
|
issue_37_raw_hex: format!("0x{issue_37_raw_u8:02x}"),
|
|
issue_38_raw_u8,
|
|
issue_38_raw_hex: format!("0x{issue_38_raw_u8:02x}"),
|
|
issue_39_raw_u8,
|
|
issue_39_raw_hex: format!("0x{issue_39_raw_u8:02x}"),
|
|
issue_3a_raw_u8,
|
|
issue_3a_raw_hex: format!("0x{issue_3a_raw_u8:02x}"),
|
|
issue_value_lane,
|
|
multiplier_lane,
|
|
issue_opinion_base_terms_raw_i32,
|
|
evidence: vec![
|
|
format!(
|
|
"chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block"
|
|
),
|
|
format!(
|
|
"next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span"
|
|
),
|
|
format!(
|
|
"issue value lane uses payload +0x{:x} ([world+0x2d]); atlas notes tie 0x004339b0 to the clamped 0..4 issue-0x37 setter there",
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"multiplier lane uses payload +0x{:x} ([world+0x29]); atlas notes tie 0x004339b0 to one companion scalar at that lane before company share-price refresh",
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"the adjacent byte strip at payload +0x{:x}..+0x{:x} carries raw issue slots 0x37..0x3a as {:02x} {:02x} {:02x} {:02x}",
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET,
|
|
RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 3,
|
|
issue_37_raw_u8,
|
|
issue_38_raw_u8,
|
|
issue_39_raw_u8,
|
|
issue_3a_raw_u8
|
|
),
|
|
],
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn parse_save_world_finance_neighborhood_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveWorldFinanceNeighborhoodProbe> {
|
|
if file_extension_hint != Some("gms") {
|
|
return None;
|
|
}
|
|
let profile = container_profile?;
|
|
let supported = matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
);
|
|
if !supported {
|
|
return None;
|
|
}
|
|
|
|
for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) {
|
|
let payload_offset = chunk_tag_offset + 4;
|
|
let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?;
|
|
if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) {
|
|
continue;
|
|
}
|
|
|
|
let current_calendar_tuple_word_lane = build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
"current_calendar_tuple_word",
|
|
RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET,
|
|
)?;
|
|
let packed_year_word_raw_u16 = read_u16_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET,
|
|
)?;
|
|
let partial_year_progress_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET + 2,
|
|
)?;
|
|
let current_calendar_tuple_word_2_lane = build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
"current_calendar_tuple_word_2",
|
|
RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET,
|
|
)?;
|
|
let absolute_counter_lane = build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
"absolute_calendar_counter",
|
|
RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET,
|
|
)?;
|
|
let absolute_counter_mirror_lane = build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
"absolute_calendar_counter_mirror",
|
|
RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_MIRROR_RELATIVE_OFFSET,
|
|
)?;
|
|
let stock_policy_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET,
|
|
)?;
|
|
let bond_policy_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET,
|
|
)?;
|
|
let bankruptcy_policy_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET,
|
|
)?;
|
|
let dividend_policy_raw_u8 = read_u8_at(
|
|
bytes,
|
|
payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET,
|
|
)?;
|
|
let building_density_growth_setting_lane = build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
"building_density_growth_setting",
|
|
RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET,
|
|
)?;
|
|
let dword_candidates =
|
|
build_save_world_finance_neighborhood_candidates(bytes, payload_offset)?;
|
|
|
|
return Some(SmpSaveWorldFinanceNeighborhoodProbe {
|
|
profile_family: profile.profile_family.clone(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-save-world-finance-neighborhood".to_string(),
|
|
chunk_tag_offset,
|
|
payload_offset,
|
|
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
|
|
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
|
|
packed_year_word_raw_u16,
|
|
packed_year_word_raw_hex: format!("0x{packed_year_word_raw_u16:04x}"),
|
|
partial_year_progress_raw_u8,
|
|
partial_year_progress_raw_hex: format!("0x{partial_year_progress_raw_u8:02x}"),
|
|
current_calendar_tuple_word_lane,
|
|
current_calendar_tuple_word_2_lane,
|
|
absolute_counter_lane,
|
|
absolute_counter_mirror_lane,
|
|
stock_policy_raw_u8,
|
|
stock_policy_raw_hex: format!("0x{stock_policy_raw_u8:02x}"),
|
|
bond_policy_raw_u8,
|
|
bond_policy_raw_hex: format!("0x{bond_policy_raw_u8:02x}"),
|
|
bankruptcy_policy_raw_u8,
|
|
bankruptcy_policy_raw_hex: format!("0x{bankruptcy_policy_raw_u8:02x}"),
|
|
dividend_policy_raw_u8,
|
|
dividend_policy_raw_hex: format!("0x{dividend_policy_raw_u8:02x}"),
|
|
building_density_growth_setting_lane,
|
|
dword_candidates,
|
|
evidence: vec![
|
|
format!(
|
|
"chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block"
|
|
),
|
|
format!(
|
|
"next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span"
|
|
),
|
|
format!(
|
|
"payload +0x{:x}/+0x{:x}/+0x{:x} carry the saved world calendar tuple and absolute counter lanes that later company stock-issue cooldown readers compare against",
|
|
RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_RELATIVE_OFFSET,
|
|
RT3_SAVE_WORLD_BLOCK_CURRENT_CALENDAR_TUPLE_WORD_2_RELATIVE_OFFSET,
|
|
RT3_SAVE_WORLD_BLOCK_ABSOLUTE_COUNTER_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"payload +0x{:x}/+0x{:x}/+0x{:x}/+0x{:x} carry the stock, bond, bankruptcy, and dividend finance-policy bytes mirrored from scenario offsets 0x4a87/0x4a8b/0x4a8f/0x4a93",
|
|
RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET,
|
|
RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET,
|
|
RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET,
|
|
RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"payload +0x{:x} carries the fixed-world building-density growth setting mirrored from `[world+0x4c7c]`, which the annual repurchase and dividend policy helpers both read directly",
|
|
RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET
|
|
),
|
|
"finance-neighborhood candidates cover the fixed dword strip around the grounded world calendar tuple, absolute-counter, selection-context, and issue-0x37 lanes so broader finance reader closure can build on one rehosted owner surface.".to_string(),
|
|
],
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn build_save_world_finance_neighborhood_candidates(
|
|
bytes: &[u8],
|
|
payload_offset: usize,
|
|
) -> Option<Vec<SmpSaveDwordCandidate>> {
|
|
(0..RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS)
|
|
.map(|index| {
|
|
let relative_offset =
|
|
RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET + index * 4;
|
|
let label = RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_CANDIDATE_FIELDS
|
|
.iter()
|
|
.find(|(_, named_offset)| *named_offset == relative_offset)
|
|
.map(|(name, _)| (*name).to_string())
|
|
.unwrap_or_else(|| format!("finance_neighborhood_word_{:02}", index + 1));
|
|
build_save_dword_candidate(bytes, payload_offset, &label, relative_offset)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn parse_save_world_economic_tuning_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveWorldEconomicTuningProbe> {
|
|
if file_extension_hint != Some("gms") {
|
|
return None;
|
|
}
|
|
let profile = container_profile?;
|
|
let supported = matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
);
|
|
if !supported {
|
|
return None;
|
|
}
|
|
|
|
for chunk_tag_offset in find_u32_le_offsets(bytes, RT3_SAVE_WORLD_BLOCK_CHUNK_TAG) {
|
|
let payload_offset = chunk_tag_offset + 4;
|
|
let next_chunk_tag_offset = payload_offset.checked_add(RT3_SAVE_WORLD_BLOCK_LEN)?;
|
|
if read_u32_at(bytes, next_chunk_tag_offset) != Some(RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG) {
|
|
continue;
|
|
}
|
|
let mirror_lane = build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
"economic_tuning_mirror_lane_0",
|
|
RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET,
|
|
)?;
|
|
let tuning_lanes = RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(lane_index, relative_offset)| {
|
|
build_save_dword_candidate(
|
|
bytes,
|
|
payload_offset,
|
|
&format!("economic_tuning_lane_{lane_index}"),
|
|
*relative_offset,
|
|
)
|
|
})
|
|
.collect::<Option<Vec<_>>>()?;
|
|
return Some(SmpSaveWorldEconomicTuningProbe {
|
|
profile_family: profile.profile_family.clone(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-save-world-economic-tuning".to_string(),
|
|
chunk_tag_offset,
|
|
payload_offset,
|
|
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
|
|
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
|
|
mirror_lane,
|
|
tuning_lanes,
|
|
evidence: vec![
|
|
format!(
|
|
"chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block"
|
|
),
|
|
format!(
|
|
"next chunk tag 0x32c9 appears at 0x{next_chunk_tag_offset:x}, matching the documented 0x4f2c payload span"
|
|
),
|
|
format!(
|
|
"mirror lane uses payload +0x{:x} ([world+0x0bde])",
|
|
RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET
|
|
),
|
|
format!(
|
|
"primary tuning lanes use payload offsets {} matching the documented [world+0x0be2..+0x0bf6] float block",
|
|
RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS
|
|
.iter()
|
|
.map(|offset| format!("0x{offset:x}"))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
),
|
|
"Current atlas evidence keeps this fixed six-float world tuning band separate from the issue-0x37 investor-confidence lane."
|
|
.to_string(),
|
|
],
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn parse_save_company_collection_header_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
|
|
parse_save_tagged_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint,
|
|
container_profile,
|
|
0x000061a9,
|
|
0x000061aa,
|
|
0x000061ab,
|
|
"save-company-tagged-header-counts",
|
|
"scenario-save-company-header-counts",
|
|
|header| {
|
|
header.direct_collection_flag == 1
|
|
&& header.live_id_bound >= 1
|
|
&& header.live_id_bound <= 0x20
|
|
&& header.live_record_count <= header.live_id_bound
|
|
&& header.direct_record_stride >= 0x1000
|
|
},
|
|
vec![
|
|
"save-side company collection uses tagged header family 0x61a9/0x61aa/0x61ab".to_string(),
|
|
"package-save per-company callback is currently grounded as a no-op stub, so this probe only claims header-level collection counts, not per-company payload".to_string(),
|
|
],
|
|
)
|
|
}
|
|
|
|
fn parse_save_chairman_profile_collection_header_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
|
|
parse_save_tagged_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint,
|
|
container_profile,
|
|
0x00005209,
|
|
0x0000520a,
|
|
0x0000520b,
|
|
"save-chairman-profile-tagged-header-counts",
|
|
"scenario-save-chairman-profile-header-counts",
|
|
|header| {
|
|
header.direct_collection_flag == 1
|
|
&& header.live_id_bound >= 1
|
|
&& header.live_id_bound <= 0x20
|
|
&& header.live_record_count <= header.live_id_bound
|
|
&& header.direct_record_stride >= 0x800
|
|
&& header.direct_record_stride <= 0x2000
|
|
},
|
|
vec![
|
|
"save-side chairman/profile collection uses tagged header family 0x5209/0x520a/0x520b".to_string(),
|
|
"the direct-record chairman/profile family is the large-stride tagged collection with embedded name and biography payload, not the smaller train-side 0x5209 family".to_string(),
|
|
],
|
|
)
|
|
}
|
|
|
|
fn parse_save_train_collection_header_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
|
|
parse_save_tagged_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint,
|
|
container_profile,
|
|
0x00005209,
|
|
0x0000520a,
|
|
0x0000520b,
|
|
"save-train-tagged-header-counts",
|
|
"scenario-save-train-header-counts",
|
|
|header| {
|
|
header.direct_collection_flag == 1
|
|
&& header.direct_record_stride >= 0x100
|
|
&& header.direct_record_stride <= 0x400
|
|
&& header.live_id_bound >= 0x10
|
|
&& header.live_id_bound <= 0x100
|
|
&& header.live_record_count >= 1
|
|
&& header.live_record_count <= header.live_id_bound
|
|
},
|
|
vec![
|
|
"save-side live train collection shares tagged header family 0x5209/0x520a/0x520b with other indexed direct-record bundles".to_string(),
|
|
"the grounded train-side candidate is the smaller direct-record family with stride 0x1d5 whose metadata payload carries Train N labels, distinct from the larger chairman/profile family and the non-direct region family".to_string(),
|
|
],
|
|
)
|
|
}
|
|
|
|
fn parse_save_train_collection_directory_probe(
|
|
bytes: &[u8],
|
|
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
) -> Option<SmpSaveTrainCollectionDirectoryProbe> {
|
|
let header_probe = header_probe?;
|
|
if header_probe.source_kind != "save-train-tagged-header-counts" {
|
|
return None;
|
|
}
|
|
let metadata_payload =
|
|
bytes.get(header_probe.metadata_tag_offset + 4..header_probe.records_tag_offset)?;
|
|
let directory_root_byte_offset =
|
|
SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX.checked_mul(4)?;
|
|
let live_record_count = header_probe.live_record_count as usize;
|
|
let directory_len_dwords =
|
|
live_record_count.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT)?;
|
|
let directory_len_bytes = directory_len_dwords.checked_mul(4)?;
|
|
let directory_bytes = metadata_payload.get(
|
|
directory_root_byte_offset..directory_root_byte_offset.checked_add(directory_len_bytes)?,
|
|
)?;
|
|
let mut entries = Vec::with_capacity(live_record_count);
|
|
for index in 0..live_record_count {
|
|
let entry_offset =
|
|
index.checked_mul(SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT * 4)?;
|
|
let payload_relative_offset = read_u32_at(directory_bytes, entry_offset)?;
|
|
let previous_live_entry_id = read_u32_at(directory_bytes, entry_offset + 4)?;
|
|
let next_live_entry_id = read_u32_at(directory_bytes, entry_offset + 8)?;
|
|
entries.push(SmpSaveTrainCollectionDirectoryEntryProbe {
|
|
live_entry_id: (index + 1) as u32,
|
|
payload_relative_offset,
|
|
payload_relative_offset_hex: format!("0x{payload_relative_offset:08x}"),
|
|
payload_absolute_offset: header_probe
|
|
.metadata_tag_offset
|
|
.checked_add(4)?
|
|
.checked_add(payload_relative_offset as usize)?,
|
|
previous_live_entry_id,
|
|
previous_live_entry_id_hex: format!("0x{previous_live_entry_id:08x}"),
|
|
next_live_entry_id,
|
|
next_live_entry_id_hex: format!("0x{next_live_entry_id:08x}"),
|
|
});
|
|
}
|
|
let chain_head_live_entry_id = entries
|
|
.iter()
|
|
.find(|entry| entry.previous_live_entry_id == 0)
|
|
.map(|entry| entry.live_entry_id);
|
|
let chain_tail_live_entry_id = entries
|
|
.iter()
|
|
.find(|entry| entry.next_live_entry_id == 0)
|
|
.map(|entry| entry.live_entry_id);
|
|
let monotonic_offsets = entries
|
|
.windows(2)
|
|
.all(|window| window[0].payload_relative_offset < window[1].payload_relative_offset);
|
|
Some(SmpSaveTrainCollectionDirectoryProbe {
|
|
profile_family: header_probe.profile_family.clone(),
|
|
source_kind: "save-train-live-directory".to_string(),
|
|
semantic_family: "scenario-save-train-live-directory".to_string(),
|
|
metadata_tag_offset: header_probe.metadata_tag_offset,
|
|
records_tag_offset: header_probe.records_tag_offset,
|
|
close_tag_offset: header_probe.close_tag_offset,
|
|
directory_root_dword_index: SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX,
|
|
directory_entry_dword_count: SAVE_REGION_COLLECTION_DIRECTORY_ENTRY_DWORD_COUNT,
|
|
live_record_count: header_probe.live_record_count,
|
|
live_id_bound: header_probe.live_id_bound,
|
|
chain_head_live_entry_id,
|
|
chain_tail_live_entry_id,
|
|
entries,
|
|
evidence: vec![
|
|
"save-side train metadata payload exposes a live-entry directory immediately after the first 16 dwords, with payload-relative offsets pointing into the later records span".to_string(),
|
|
format!(
|
|
"train live directory decodes {} triplets of (payload_relative_offset, prev_live_entry_id, next_live_entry_id) from metadata dword index {}",
|
|
header_probe.live_record_count,
|
|
SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX
|
|
),
|
|
format!(
|
|
"decoded directory preserves a head/tail chain {:?}->{:?} and monotonic payload offsets={monotonic_offsets}",
|
|
chain_head_live_entry_id, chain_tail_live_entry_id
|
|
),
|
|
],
|
|
})
|
|
}
|
|
|
|
fn parse_save_region_collection_header_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
|
|
let probe = parse_save_tagged_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint,
|
|
container_profile,
|
|
0x00005209,
|
|
0x0000520a,
|
|
0x0000520b,
|
|
"save-region-tagged-header-counts",
|
|
"scenario-save-region-header-counts",
|
|
|header| {
|
|
header.direct_collection_flag == 0
|
|
&& header.direct_record_stride == 0x06
|
|
&& header.live_id_bound >= 0x80
|
|
&& header.live_id_bound <= 0x200
|
|
&& header.live_record_count >= 0x80
|
|
&& header.live_record_count <= header.live_id_bound
|
|
},
|
|
vec![
|
|
"save-side live region collection shares tagged header family 0x5209/0x520a/0x520b with trains and chairman profiles, but uses the larger non-direct indexed family".to_string(),
|
|
"the grounded region-side candidate is the non-direct 0x5209 family with live_id_bound/count in the 0x96/0x91 range and Marker09-style default stems in the records span, distinct from the smaller direct train family".to_string(),
|
|
],
|
|
)?;
|
|
let records_preview = bytes
|
|
.get(probe.records_tag_offset + 4..probe.close_tag_offset)
|
|
.unwrap_or(&[]);
|
|
records_preview
|
|
.windows("Marker09".len())
|
|
.any(|window| window == b"Marker09")
|
|
.then_some(probe)
|
|
}
|
|
|
|
fn parse_save_region_record_triplet_probe(
|
|
bytes: &[u8],
|
|
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
) -> Option<SmpSaveRegionRecordTripletProbe> {
|
|
let header_probe = header_probe?;
|
|
if header_probe.source_kind != "save-region-tagged-header-counts" {
|
|
return None;
|
|
}
|
|
let records_payload =
|
|
bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?;
|
|
let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG);
|
|
let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG);
|
|
let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG);
|
|
let record_count = header_probe.live_record_count as usize;
|
|
if name_offsets.len() != record_count
|
|
|| policy_offsets.len() != record_count
|
|
|| profile_offsets.len() != record_count
|
|
{
|
|
return None;
|
|
}
|
|
let mut entries = Vec::with_capacity(record_count);
|
|
for index in 0..record_count {
|
|
let name_tag_relative_offset = name_offsets[index];
|
|
let policy_tag_relative_offset = policy_offsets[index];
|
|
let profile_tag_relative_offset = profile_offsets[index];
|
|
let next_record_relative_offset = name_offsets
|
|
.get(index + 1)
|
|
.copied()
|
|
.unwrap_or(records_payload.len());
|
|
if !(name_tag_relative_offset < policy_tag_relative_offset
|
|
&& policy_tag_relative_offset < profile_tag_relative_offset
|
|
&& profile_tag_relative_offset < next_record_relative_offset)
|
|
{
|
|
return None;
|
|
}
|
|
let name_payload =
|
|
records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?;
|
|
let name = parse_save_len_prefixed_ascii_name(name_payload)?;
|
|
let policy_chunk_len =
|
|
profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?;
|
|
if policy_chunk_len != 0x1a {
|
|
return None;
|
|
}
|
|
let policy_payload =
|
|
records_payload.get(policy_tag_relative_offset + 4..profile_tag_relative_offset)?;
|
|
let policy_leading_f32_0 = f32::from_bits(read_u32_at(policy_payload, 0)?);
|
|
let policy_leading_f32_1 = f32::from_bits(read_u32_at(policy_payload, 4)?);
|
|
let policy_leading_f32_2 = f32::from_bits(read_u32_at(policy_payload, 8)?);
|
|
let mut policy_reserved_dwords = Vec::with_capacity(3);
|
|
for dword_index in 0..3 {
|
|
policy_reserved_dwords.push(read_u32_at(policy_payload, 12 + dword_index * 4)?);
|
|
}
|
|
let policy_trailing_word = read_u16_at(policy_payload, 24)?;
|
|
let profile_chunk_len =
|
|
next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?;
|
|
let profile_payload =
|
|
records_payload.get(profile_tag_relative_offset + 4..next_record_relative_offset)?;
|
|
let profile_collection = parse_save_region_profile_collection_probe(profile_payload);
|
|
entries.push(SmpSaveRegionRecordTripletEntryProbe {
|
|
record_index: index,
|
|
name,
|
|
name_tag_relative_offset,
|
|
policy_tag_relative_offset,
|
|
profile_tag_relative_offset,
|
|
policy_chunk_len,
|
|
profile_chunk_len,
|
|
policy_leading_f32_0,
|
|
policy_leading_f32_1,
|
|
policy_leading_f32_2,
|
|
policy_reserved_dwords,
|
|
policy_trailing_word,
|
|
policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"),
|
|
profile_collection,
|
|
});
|
|
}
|
|
let zero_trailing_padding_record_count = entries
|
|
.iter()
|
|
.filter(|entry| {
|
|
entry
|
|
.profile_collection
|
|
.as_ref()
|
|
.is_some_and(|collection| collection.trailing_padding_len == 0)
|
|
})
|
|
.count();
|
|
Some(SmpSaveRegionRecordTripletProbe {
|
|
profile_family: header_probe.profile_family.clone(),
|
|
source_kind: "save-region-record-triplets".to_string(),
|
|
semantic_family: "scenario-save-region-record-triplets".to_string(),
|
|
records_tag_offset: header_probe.records_tag_offset,
|
|
close_tag_offset: header_probe.close_tag_offset,
|
|
record_count,
|
|
entries,
|
|
evidence: vec![
|
|
"save-side region records in the non-direct Marker09 family are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the records span".to_string(),
|
|
format!(
|
|
"decoded {} region record triplets with one len-prefixed name chunk, one fixed policy chunk, and one trailing profile payload chunk per record",
|
|
record_count
|
|
),
|
|
"each fixed 0x55f2 policy chunk currently decodes as three leading f32 lanes, three reserved dwords, and one trailing u16 word".to_string(),
|
|
"the trailing 0x55f3 payload also carries an embedded direct profile collection with fixed 0x22-byte rows on grounded saves".to_string(),
|
|
format!(
|
|
"on grounded saves the 0x55f3 payload is fully consumed by that embedded profile collection: all {} decoded records currently have zero trailing padding beyond the direct profile rows",
|
|
zero_trailing_padding_record_count
|
|
),
|
|
],
|
|
})
|
|
}
|
|
|
|
fn parse_save_region_queued_notice_record_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
region_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
) -> Option<SmpSaveRegionQueuedNoticeRecordProbe> {
|
|
if file_extension_hint != Some("gms") {
|
|
return None;
|
|
}
|
|
let profile = container_profile?;
|
|
let max_region_id = region_header_probe
|
|
.map(|probe| probe.live_id_bound)
|
|
.unwrap_or(0x1000);
|
|
let entries = find_u32_le_offsets(bytes, SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED)
|
|
.into_iter()
|
|
.filter_map(|payload_seed_offset| {
|
|
let node_base_offset = payload_seed_offset.checked_sub(4)?;
|
|
let _node_bytes = bytes
|
|
.get(node_base_offset..node_base_offset + SAVE_REGION_QUEUED_NOTICE_NODE_LEN)?;
|
|
let next_link_raw = read_u32_at(bytes, node_base_offset)?;
|
|
let kind = read_u32_at(bytes, node_base_offset + 8)?;
|
|
let promotion_latch_dword = read_u32_at(bytes, node_base_offset + 12)?;
|
|
let region_id = read_u32_at(bytes, node_base_offset + 16)?;
|
|
let amount = read_u32_at(bytes, node_base_offset + 20)?;
|
|
let trailing_sentinel_i32_0 = read_i32_at(bytes, node_base_offset + 24)?;
|
|
let trailing_sentinel_i32_1 = read_i32_at(bytes, node_base_offset + 28)?;
|
|
if !(kind == SAVE_REGION_QUEUED_NOTICE_NODE_KIND
|
|
&& promotion_latch_dword == 0
|
|
&& region_id >= 1
|
|
&& region_id <= max_region_id
|
|
&& amount > 0
|
|
&& trailing_sentinel_i32_0 == -1
|
|
&& trailing_sentinel_i32_1 == -1)
|
|
{
|
|
return None;
|
|
}
|
|
Some(SmpSaveRegionQueuedNoticeRecordEntryProbe {
|
|
node_base_offset,
|
|
payload_seed_offset,
|
|
next_link_raw,
|
|
next_link_raw_hex: format!("0x{next_link_raw:08x}"),
|
|
payload_seed_dword: SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED,
|
|
payload_seed_dword_hex: format!("0x{SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED:08x}"),
|
|
kind,
|
|
kind_hex: format!("0x{kind:08x}"),
|
|
promotion_latch_dword,
|
|
promotion_latch_dword_hex: format!("0x{promotion_latch_dword:08x}"),
|
|
region_id,
|
|
region_id_hex: format!("0x{region_id:08x}"),
|
|
amount,
|
|
amount_hex: format!("0x{amount:08x}"),
|
|
trailing_sentinel_i32_0,
|
|
trailing_sentinel_i32_0_hex: format!("0x{:08x}", trailing_sentinel_i32_0 as u32),
|
|
trailing_sentinel_i32_1,
|
|
trailing_sentinel_i32_1_hex: format!("0x{:08x}", trailing_sentinel_i32_1 as u32),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if entries.is_empty() {
|
|
return None;
|
|
}
|
|
Some(SmpSaveRegionQueuedNoticeRecordProbe {
|
|
profile_family: profile.profile_family.clone(),
|
|
source_kind: "save-region-queued-notice-records".to_string(),
|
|
semantic_family: "scenario-save-region-queued-notice-records".to_string(),
|
|
payload_seed_dword: SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED,
|
|
payload_seed_dword_hex: format!("0x{SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED:08x}"),
|
|
entries,
|
|
evidence: vec![
|
|
"save-side scan searches for the grounded region queued-notice payload seed 0x005c87a8 and validates the full 0x20-byte node shape from the atlas-backed queue owner".to_string(),
|
|
"accepted nodes require kind=7, promotion-latch dword=0, a bounded live region id, a positive amount, and trailing sentinel dwords -1/-1".to_string(),
|
|
],
|
|
})
|
|
}
|
|
|
|
fn parse_save_placed_structure_record_triplet_probe(
|
|
bytes: &[u8],
|
|
header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
|
|
) -> Option<SmpSavePlacedStructureRecordTripletProbe> {
|
|
let header_probe = header_probe?;
|
|
if header_probe.source_kind != "save-placed-structure-tagged-header-counts" {
|
|
return None;
|
|
}
|
|
let records_payload =
|
|
bytes.get(header_probe.records_tag_offset + 4..header_probe.close_tag_offset)?;
|
|
let name_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG);
|
|
let policy_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_POLICY_TAG);
|
|
let profile_offsets = find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_PROFILE_TAG);
|
|
let record_count = header_probe.live_record_count as usize;
|
|
if name_offsets.len() != record_count
|
|
|| policy_offsets.len() != record_count
|
|
|| profile_offsets.len() != record_count
|
|
{
|
|
return None;
|
|
}
|
|
let mut entries = Vec::with_capacity(record_count);
|
|
for index in 0..record_count {
|
|
let name_tag_relative_offset = name_offsets[index];
|
|
let policy_tag_relative_offset = policy_offsets[index];
|
|
let profile_tag_relative_offset = profile_offsets[index];
|
|
let next_record_relative_offset = name_offsets
|
|
.get(index + 1)
|
|
.copied()
|
|
.unwrap_or(records_payload.len());
|
|
if !(name_tag_relative_offset < policy_tag_relative_offset
|
|
&& policy_tag_relative_offset < profile_tag_relative_offset
|
|
&& profile_tag_relative_offset < next_record_relative_offset)
|
|
{
|
|
return None;
|
|
}
|
|
let name_payload =
|
|
records_payload.get(name_tag_relative_offset + 4..policy_tag_relative_offset)?;
|
|
let (primary_name, secondary_name) = parse_save_len_prefixed_ascii_name_pair(name_payload)?;
|
|
let policy_chunk_len =
|
|
profile_tag_relative_offset.checked_sub(policy_tag_relative_offset + 4)?;
|
|
if policy_chunk_len != 0x1a {
|
|
return None;
|
|
}
|
|
let policy_payload =
|
|
records_payload.get(policy_tag_relative_offset + 4..profile_tag_relative_offset)?;
|
|
let policy_f32_lane_0 = f32::from_bits(read_u32_at(policy_payload, 0)?);
|
|
let policy_f32_lane_1 = f32::from_bits(read_u32_at(policy_payload, 4)?);
|
|
let policy_f32_lane_2 = f32::from_bits(read_u32_at(policy_payload, 8)?);
|
|
let policy_f32_lane_3 = f32::from_bits(read_u32_at(policy_payload, 12)?);
|
|
let policy_f32_lane_4 = f32::from_bits(read_u32_at(policy_payload, 16)?);
|
|
let policy_reserved_dword = read_u32_at(policy_payload, 20)?;
|
|
let policy_trailing_word = read_u16_at(policy_payload, 24)?;
|
|
let profile_chunk_len =
|
|
next_record_relative_offset.checked_sub(profile_tag_relative_offset + 4)?;
|
|
let profile_payload =
|
|
records_payload.get(profile_tag_relative_offset + 4..next_record_relative_offset)?;
|
|
let profile_open_marker = read_u32_at(profile_payload, 0)?;
|
|
if profile_open_marker != 0x00005dc1 {
|
|
return None;
|
|
}
|
|
let (profile_repeated_primary_name, profile_repeated_secondary_name) =
|
|
parse_save_len_prefixed_ascii_name_pair(profile_payload.get(4..)?)?;
|
|
let mut trailer_offset = 4usize;
|
|
let repeated_primary_len = *profile_payload.get(trailer_offset)? as usize;
|
|
trailer_offset += 1 + repeated_primary_len;
|
|
while matches!(profile_payload.get(trailer_offset), Some(0)) {
|
|
trailer_offset += 1;
|
|
}
|
|
let repeated_secondary_len = *profile_payload.get(trailer_offset)? as usize;
|
|
trailer_offset += 1 + repeated_secondary_len;
|
|
let mut matched_footer = None;
|
|
for candidate_offset in [trailer_offset, trailer_offset + 1] {
|
|
if let (
|
|
Some(profile_payload_dword),
|
|
Some(profile_sentinel_i32),
|
|
Some(profile_close_marker),
|
|
) = (
|
|
read_u32_at(profile_payload, candidate_offset),
|
|
read_i32_at(profile_payload, candidate_offset + 4),
|
|
read_u32_at(profile_payload, candidate_offset + 8),
|
|
) {
|
|
if profile_close_marker == 0x00005dc2 {
|
|
matched_footer = Some((
|
|
profile_payload_dword,
|
|
profile_sentinel_i32,
|
|
profile_close_marker,
|
|
));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
let (profile_payload_dword, profile_sentinel_i32, profile_close_marker) = matched_footer?;
|
|
let (profile_status_kind, farm_growth_stage_index) =
|
|
derive_save_placed_structure_profile_status(
|
|
&primary_name,
|
|
&secondary_name,
|
|
profile_sentinel_i32,
|
|
);
|
|
entries.push(SmpSavePlacedStructureRecordTripletEntryProbe {
|
|
record_index: index,
|
|
primary_name,
|
|
secondary_name,
|
|
name_tag_relative_offset,
|
|
policy_tag_relative_offset,
|
|
profile_tag_relative_offset,
|
|
policy_chunk_len,
|
|
profile_chunk_len,
|
|
policy_f32_lane_0,
|
|
policy_f32_lane_1,
|
|
policy_f32_lane_2,
|
|
policy_f32_lane_3,
|
|
policy_f32_lane_4,
|
|
policy_reserved_dword,
|
|
policy_trailing_word,
|
|
policy_trailing_word_hex: format!("0x{policy_trailing_word:04x}"),
|
|
profile_open_marker,
|
|
profile_open_marker_hex: format!("0x{profile_open_marker:08x}"),
|
|
profile_repeated_primary_name,
|
|
profile_repeated_secondary_name,
|
|
profile_payload_dword,
|
|
profile_payload_dword_hex: format!("0x{profile_payload_dword:08x}"),
|
|
profile_sentinel_i32,
|
|
profile_status_kind: profile_status_kind.to_string(),
|
|
farm_growth_stage_index,
|
|
profile_close_marker,
|
|
profile_close_marker_hex: format!("0x{profile_close_marker:08x}"),
|
|
});
|
|
}
|
|
let farm_growth_stage_entry_count = entries
|
|
.iter()
|
|
.filter(|entry| entry.farm_growth_stage_index.is_some())
|
|
.count();
|
|
Some(SmpSavePlacedStructureRecordTripletProbe {
|
|
profile_family: header_probe.profile_family.clone(),
|
|
source_kind: "save-placed-structure-record-triplets".to_string(),
|
|
semantic_family: "scenario-save-placed-structure-record-triplets".to_string(),
|
|
records_tag_offset: header_probe.records_tag_offset,
|
|
close_tag_offset: header_probe.close_tag_offset,
|
|
record_count,
|
|
entries,
|
|
evidence: vec![
|
|
"save-side placed-structure records are serialized as repeated 0x55f1/0x55f2/0x55f3 triplets inside the tagged records span".to_string(),
|
|
"the 0x55f1 chunk currently exposes two len-prefixed structure-name stems before the fixed 0x55f2 policy row".to_string(),
|
|
"each fixed placed-structure 0x55f2 policy chunk currently decodes as five f32-like lanes, one reserved dword, and one trailing u16 word".to_string(),
|
|
format!(
|
|
"the compact 0x55f3 footer status lane behaves like a farm growth-stage bucket on grounded saves: {farm_growth_stage_entry_count} entries expose nonnegative 0..11 values and all observed non-farm families stay at -1"
|
|
),
|
|
],
|
|
})
|
|
}
|
|
|
|
fn derive_save_placed_structure_profile_status(
|
|
primary_name: &str,
|
|
secondary_name: &str,
|
|
raw_status: i32,
|
|
) -> (&'static str, Option<u8>) {
|
|
let looks_like_farm = primary_name.starts_with("Farm") || secondary_name.contains("Farm");
|
|
if raw_status == -1 {
|
|
return ("unset", None);
|
|
}
|
|
if looks_like_farm && (0..=11).contains(&raw_status) {
|
|
return ("farm_growth_stage_bucket", Some(raw_status as u8));
|
|
}
|
|
("opaque_nondefault", None)
|
|
}
|
|
|
|
fn parse_save_placed_structure_collection_header_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
|
|
parse_save_tagged_collection_header_probe(
|
|
bytes,
|
|
file_extension_hint,
|
|
container_profile,
|
|
0x000036b1,
|
|
0x000036b2,
|
|
0x000036b3,
|
|
"save-placed-structure-tagged-header-counts",
|
|
"scenario-save-placed-structure-header-counts",
|
|
|header| {
|
|
header.direct_collection_flag == 0
|
|
&& header.direct_record_stride >= 1
|
|
&& header.direct_record_stride <= 0x20
|
|
&& header.live_id_bound >= 0x100
|
|
&& header.live_record_count >= 0x100
|
|
&& header.live_record_count <= header.live_id_bound
|
|
},
|
|
vec![
|
|
"save-side placed-structure collection uses tagged family 0x36b1/0x36b2/0x36b3 beneath the wider local-runtime and route-entry rebuild owners".to_string(),
|
|
"current evidence only grounds header-level placed-structure collection counts here; direct record-body reconstruction still needs the later per-entry load/save slot study.".to_string(),
|
|
],
|
|
)
|
|
}
|
|
|
|
fn parse_save_placed_structure_dynamic_side_buffer_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSavePlacedStructureDynamicSideBufferProbe> {
|
|
#[derive(Clone)]
|
|
struct EmbeddedNameRow {
|
|
name_tag_relative_offset: usize,
|
|
prefix_leading_dword: u32,
|
|
prefix_trailing_word: u16,
|
|
prefix_separator_byte: u8,
|
|
primary_name: Option<String>,
|
|
secondary_name: Option<String>,
|
|
tertiary_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct PrefixPatternAccumulator {
|
|
count: usize,
|
|
first_name_tag_relative_offset: usize,
|
|
first_primary_name: Option<String>,
|
|
first_secondary_name: Option<String>,
|
|
section_like_primary_name_count: usize,
|
|
cap_like_primary_name_count: usize,
|
|
other_primary_name_count: usize,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct NamePairAccumulator {
|
|
count: usize,
|
|
first_name_tag_relative_offset: usize,
|
|
prefix_counts: BTreeMap<(u32, u16, u8), usize>,
|
|
}
|
|
|
|
if file_extension_hint != Some("gms") {
|
|
return None;
|
|
}
|
|
let profile = container_profile?;
|
|
if !matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
) {
|
|
return None;
|
|
}
|
|
|
|
let metadata_offsets = find_u32_le_offsets(bytes, 0x000038a5);
|
|
let records_offsets = find_u32_le_offsets(bytes, 0x000038a6);
|
|
let close_offsets = find_u32_le_offsets(bytes, 0x000038a7);
|
|
for metadata_tag_offset in metadata_offsets {
|
|
let Some(records_tag_offset) = records_offsets
|
|
.iter()
|
|
.copied()
|
|
.find(|offset| *offset > metadata_tag_offset)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(close_tag_offset) = close_offsets
|
|
.iter()
|
|
.copied()
|
|
.find(|offset| *offset > records_tag_offset)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(payload) = bytes.get(metadata_tag_offset + 4..records_tag_offset) else {
|
|
continue;
|
|
};
|
|
if payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN {
|
|
continue;
|
|
}
|
|
let Some(header_words) = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT)
|
|
.map(|index| read_u32_at(payload, index * 4))
|
|
.collect::<Option<Vec<_>>>()
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(header_words): Option<[u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT]> =
|
|
header_words.try_into().ok()
|
|
else {
|
|
continue;
|
|
};
|
|
let summary = IndexedCollectionHeaderSummary {
|
|
metadata_tag_offset,
|
|
records_tag_offset,
|
|
close_tag_offset,
|
|
direct_collection_flag: header_words[0],
|
|
direct_record_stride: header_words[1],
|
|
live_id_bound: header_words[4],
|
|
live_record_count: header_words[5],
|
|
header_words,
|
|
};
|
|
if !(summary.direct_collection_flag == 0
|
|
&& summary.direct_record_stride == 0x06
|
|
&& summary.header_words.get(2) == Some(&1000)
|
|
&& summary.header_words.get(3) == Some(&500)
|
|
&& summary.header_words.get(6) == Some(&0)
|
|
&& summary.header_words.get(7) == Some(&1)
|
|
&& summary.live_id_bound >= 0x100
|
|
&& summary.live_id_bound <= 0x1000
|
|
&& summary.live_record_count >= 0x100
|
|
&& summary.live_record_count <= summary.live_id_bound)
|
|
{
|
|
continue;
|
|
}
|
|
let Some(records_payload) = bytes.get(records_tag_offset + 4..close_tag_offset) else {
|
|
continue;
|
|
};
|
|
let embedded_name_tag_offsets =
|
|
find_u16_le_offsets(records_payload, SAVE_REGION_RECORD_NAME_TAG);
|
|
let Some(&first_embedded_name_tag_relative_offset) = embedded_name_tag_offsets.first()
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(prefix_payload) = records_payload.get(..first_embedded_name_tag_relative_offset)
|
|
else {
|
|
continue;
|
|
};
|
|
if prefix_payload.len() < 7 {
|
|
continue;
|
|
}
|
|
let Some(owner_shared_dword) = read_u32_at(prefix_payload, 0) else {
|
|
continue;
|
|
};
|
|
let owner_shared_dword_relative_offset = 0usize;
|
|
let prefix_leading_dword = owner_shared_dword;
|
|
let Some(prefix_trailing_word) = read_u16_at(prefix_payload, 4) else {
|
|
continue;
|
|
};
|
|
let Some(prefix_separator_byte) = prefix_payload.get(6).copied() else {
|
|
continue;
|
|
};
|
|
let mut parsed_embedded_names = None;
|
|
for relative_name_offset in [4usize, 6usize] {
|
|
let Some(name_payload) = records_payload
|
|
.get(first_embedded_name_tag_relative_offset + relative_name_offset..)
|
|
else {
|
|
continue;
|
|
};
|
|
if let Some(names) = parse_save_len_prefixed_ascii_name_triplet(name_payload) {
|
|
parsed_embedded_names = Some(names);
|
|
break;
|
|
}
|
|
}
|
|
let Some((
|
|
first_embedded_primary_name,
|
|
first_embedded_secondary_name,
|
|
first_embedded_tertiary_name,
|
|
)) = parsed_embedded_names
|
|
else {
|
|
continue;
|
|
};
|
|
let embedded_name_rows = embedded_name_tag_offsets
|
|
.iter()
|
|
.copied()
|
|
.filter_map(|name_tag_relative_offset| {
|
|
let prefix_payload = records_payload.get(..name_tag_relative_offset)?;
|
|
if prefix_payload.len() < 7 {
|
|
return None;
|
|
}
|
|
let prefix_leading_dword = read_u32_at(prefix_payload, prefix_payload.len() - 7)?;
|
|
let prefix_trailing_word = read_u16_at(prefix_payload, prefix_payload.len() - 3)?;
|
|
let prefix_separator_byte = *prefix_payload.last()?;
|
|
let mut parsed_names = None;
|
|
for relative_name_offset in [4usize, 6usize] {
|
|
let Some(name_payload) =
|
|
records_payload.get(name_tag_relative_offset + relative_name_offset..)
|
|
else {
|
|
continue;
|
|
};
|
|
if let Some(names) = parse_save_len_prefixed_ascii_name_triplet(name_payload) {
|
|
parsed_names = Some(names);
|
|
break;
|
|
}
|
|
}
|
|
let (primary_name, secondary_name, tertiary_name) =
|
|
parsed_names.unwrap_or_default();
|
|
Some(EmbeddedNameRow {
|
|
name_tag_relative_offset,
|
|
prefix_leading_dword,
|
|
prefix_trailing_word,
|
|
prefix_separator_byte,
|
|
primary_name: (!primary_name.is_empty()).then_some(primary_name),
|
|
secondary_name: (!secondary_name.is_empty()).then_some(secondary_name),
|
|
tertiary_name,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let embedded_name_row_samples = embedded_name_rows
|
|
.iter()
|
|
.take(8)
|
|
.enumerate()
|
|
.map(
|
|
|(sample_index, row)| SmpSavePlacedStructureDynamicSideBufferSampleEntry {
|
|
sample_index,
|
|
name_tag_relative_offset: row.name_tag_relative_offset,
|
|
prefix_leading_dword: row.prefix_leading_dword,
|
|
prefix_leading_dword_hex: format!("0x{:08x}", row.prefix_leading_dword),
|
|
prefix_trailing_word: row.prefix_trailing_word,
|
|
prefix_trailing_word_hex: format!("0x{:04x}", row.prefix_trailing_word),
|
|
prefix_separator_byte: row.prefix_separator_byte,
|
|
prefix_separator_byte_hex: format!("0x{:02x}", row.prefix_separator_byte),
|
|
primary_name: row.primary_name.clone(),
|
|
secondary_name: row.secondary_name.clone(),
|
|
tertiary_name: row.tertiary_name.clone(),
|
|
},
|
|
)
|
|
.collect::<Vec<_>>();
|
|
let mut compact_prefix_pattern_map =
|
|
BTreeMap::<(u32, u16, u8), PrefixPatternAccumulator>::new();
|
|
let mut name_pair_map = BTreeMap::<(String, String), NamePairAccumulator>::new();
|
|
for row in &embedded_name_rows {
|
|
let entry = compact_prefix_pattern_map
|
|
.entry((
|
|
row.prefix_leading_dword,
|
|
row.prefix_trailing_word,
|
|
row.prefix_separator_byte,
|
|
))
|
|
.or_insert_with(|| PrefixPatternAccumulator {
|
|
first_name_tag_relative_offset: row.name_tag_relative_offset,
|
|
first_primary_name: row.primary_name.clone(),
|
|
first_secondary_name: row.secondary_name.clone(),
|
|
..Default::default()
|
|
});
|
|
entry.count += 1;
|
|
match row.primary_name.as_deref() {
|
|
Some(name) if name.ends_with("_Section.3dp") => {
|
|
entry.section_like_primary_name_count += 1;
|
|
}
|
|
Some(name) if name.ends_with("_Cap.3dp") => {
|
|
entry.cap_like_primary_name_count += 1;
|
|
}
|
|
_ => {
|
|
entry.other_primary_name_count += 1;
|
|
}
|
|
}
|
|
if let (Some(primary_name), Some(secondary_name)) =
|
|
(row.primary_name.as_ref(), row.secondary_name.as_ref())
|
|
{
|
|
let entry = name_pair_map
|
|
.entry((primary_name.clone(), secondary_name.clone()))
|
|
.or_insert_with(|| NamePairAccumulator {
|
|
first_name_tag_relative_offset: row.name_tag_relative_offset,
|
|
..Default::default()
|
|
});
|
|
entry.count += 1;
|
|
*entry
|
|
.prefix_counts
|
|
.entry((
|
|
row.prefix_leading_dword,
|
|
row.prefix_trailing_word,
|
|
row.prefix_separator_byte,
|
|
))
|
|
.or_default() += 1;
|
|
}
|
|
}
|
|
let prefix_leading_dword_matching_embedded_profile_tag_count = embedded_name_rows
|
|
.iter()
|
|
.filter(|row| row.prefix_leading_dword == u32::from(SAVE_REGION_RECORD_PROFILE_TAG))
|
|
.count();
|
|
let mut compact_prefix_pattern_summaries = compact_prefix_pattern_map
|
|
.into_iter()
|
|
.map(
|
|
|(
|
|
(prefix_leading_dword, prefix_trailing_word, prefix_separator_byte),
|
|
accumulator,
|
|
)| {
|
|
SmpSavePlacedStructureDynamicSideBufferPrefixPatternSummary {
|
|
prefix_leading_dword,
|
|
prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"),
|
|
prefix_trailing_word,
|
|
prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"),
|
|
prefix_separator_byte,
|
|
prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"),
|
|
count: accumulator.count,
|
|
first_name_tag_relative_offset: accumulator.first_name_tag_relative_offset,
|
|
prefix_leading_dword_matches_embedded_profile_tag: prefix_leading_dword
|
|
== u32::from(SAVE_REGION_RECORD_PROFILE_TAG),
|
|
section_like_primary_name_count: accumulator
|
|
.section_like_primary_name_count,
|
|
cap_like_primary_name_count: accumulator.cap_like_primary_name_count,
|
|
other_primary_name_count: accumulator.other_primary_name_count,
|
|
first_primary_name: accumulator.first_primary_name,
|
|
first_secondary_name: accumulator.first_secondary_name,
|
|
}
|
|
},
|
|
)
|
|
.collect::<Vec<_>>();
|
|
compact_prefix_pattern_summaries.sort_by(|left, right| {
|
|
right
|
|
.count
|
|
.cmp(&left.count)
|
|
.then_with(|| {
|
|
left.first_name_tag_relative_offset
|
|
.cmp(&right.first_name_tag_relative_offset)
|
|
})
|
|
.then_with(|| left.prefix_leading_dword.cmp(&right.prefix_leading_dword))
|
|
.then_with(|| left.prefix_trailing_word.cmp(&right.prefix_trailing_word))
|
|
.then_with(|| left.prefix_separator_byte.cmp(&right.prefix_separator_byte))
|
|
});
|
|
let mut name_pair_summaries = name_pair_map
|
|
.into_iter()
|
|
.filter_map(|((primary_name, secondary_name), accumulator)| {
|
|
let dominant_prefix = accumulator.prefix_counts.iter().max_by(
|
|
|(left_key, left_count), (right_key, right_count)| {
|
|
left_count
|
|
.cmp(right_count)
|
|
.then_with(|| right_key.cmp(left_key))
|
|
},
|
|
)?;
|
|
let (
|
|
dominant_prefix_leading_dword,
|
|
dominant_prefix_trailing_word,
|
|
dominant_prefix_separator_byte,
|
|
) = *dominant_prefix.0;
|
|
let dominant_prefix_count = *dominant_prefix.1;
|
|
Some(SmpSavePlacedStructureDynamicSideBufferNamePairSummary {
|
|
primary_name,
|
|
secondary_name,
|
|
count: accumulator.count,
|
|
first_name_tag_relative_offset: accumulator.first_name_tag_relative_offset,
|
|
unique_compact_prefix_pattern_count: accumulator.prefix_counts.len(),
|
|
dominant_prefix_leading_dword,
|
|
dominant_prefix_leading_dword_hex: format!(
|
|
"0x{dominant_prefix_leading_dword:08x}"
|
|
),
|
|
dominant_prefix_trailing_word,
|
|
dominant_prefix_trailing_word_hex: format!(
|
|
"0x{dominant_prefix_trailing_word:04x}"
|
|
),
|
|
dominant_prefix_separator_byte,
|
|
dominant_prefix_separator_byte_hex: format!(
|
|
"0x{dominant_prefix_separator_byte:02x}"
|
|
),
|
|
dominant_prefix_count,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
name_pair_summaries.sort_by(|left, right| {
|
|
right
|
|
.count
|
|
.cmp(&left.count)
|
|
.then_with(|| {
|
|
left.first_name_tag_relative_offset
|
|
.cmp(&right.first_name_tag_relative_offset)
|
|
})
|
|
.then_with(|| left.primary_name.cmp(&right.primary_name))
|
|
.then_with(|| left.secondary_name.cmp(&right.secondary_name))
|
|
});
|
|
let unique_embedded_name_pair_count = name_pair_summaries.len();
|
|
let dominant_compact_prefix_pattern = compact_prefix_pattern_summaries.first().cloned();
|
|
let decoded_embedded_name_row_count = embedded_name_rows
|
|
.iter()
|
|
.filter(|row| row.primary_name.is_some() && row.secondary_name.is_some())
|
|
.count();
|
|
let decoded_embedded_name_row_with_tertiary_name_count = embedded_name_rows
|
|
.iter()
|
|
.filter(|row| {
|
|
row.primary_name.is_some()
|
|
&& row.secondary_name.is_some()
|
|
&& row.tertiary_name.is_some()
|
|
})
|
|
.count();
|
|
return Some(SmpSavePlacedStructureDynamicSideBufferProbe {
|
|
profile_family: profile.profile_family.clone(),
|
|
source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(),
|
|
semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records".to_string(),
|
|
metadata_tag_offset,
|
|
records_tag_offset,
|
|
close_tag_offset,
|
|
records_span_len: close_tag_offset.saturating_sub(records_tag_offset + 4),
|
|
direct_record_stride: summary.direct_record_stride,
|
|
direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride),
|
|
live_id_bound: summary.live_id_bound,
|
|
live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound),
|
|
live_record_count: summary.live_record_count,
|
|
live_record_count_hex: format!("0x{:08x}", summary.live_record_count),
|
|
owner_shared_dword,
|
|
owner_shared_dword_hex: format!("0x{owner_shared_dword:08x}"),
|
|
owner_shared_dword_relative_offset,
|
|
owner_shared_dword_matches_first_compact_prefix_leading_dword:
|
|
owner_shared_dword == prefix_leading_dword,
|
|
prefix_leading_dword,
|
|
prefix_leading_dword_hex: format!("0x{prefix_leading_dword:08x}"),
|
|
prefix_trailing_word,
|
|
prefix_trailing_word_hex: format!("0x{prefix_trailing_word:04x}"),
|
|
prefix_separator_byte,
|
|
prefix_separator_byte_hex: format!("0x{prefix_separator_byte:02x}"),
|
|
first_embedded_name_tag_relative_offset,
|
|
embedded_name_tag_count: embedded_name_tag_offsets.len(),
|
|
decoded_embedded_name_row_count,
|
|
decoded_embedded_name_row_with_tertiary_name_count,
|
|
unique_compact_prefix_pattern_count: compact_prefix_pattern_summaries.len(),
|
|
prefix_leading_dword_matching_embedded_profile_tag_count,
|
|
unique_embedded_name_pair_count,
|
|
first_embedded_primary_name: Some(first_embedded_primary_name.clone()),
|
|
first_embedded_secondary_name: Some(first_embedded_secondary_name.clone()),
|
|
first_embedded_tertiary_name: first_embedded_tertiary_name.clone(),
|
|
embedded_name_row_samples,
|
|
compact_prefix_pattern_summaries,
|
|
name_pair_summaries,
|
|
evidence: vec![
|
|
"exact little-endian u32 tag family 0x38a5/0x38a6/0x38a7 appears as a separate save-side tagged collection on grounded saves".to_string(),
|
|
format!(
|
|
"direct disassembly now shows 0x00493be0 consuming shared owner-local dword 0x{owner_shared_dword:08x} from the 0x38a6 stream before iterating live infrastructure records"
|
|
),
|
|
"records payload begins with a compact 6-byte prefix plus one separator byte before the first embedded 0x55f1 name row".to_string(),
|
|
"first embedded 0x55f1 row decodes with placed-structure-style dual names, which makes this the strongest current candidate for the separate placed-structure dynamic side-buffer owner seam".to_string(),
|
|
format!(
|
|
"grounded first embedded names are {:?}/{:?}/{:?} with {} embedded 0x55f1 name rows in the tagged records span",
|
|
Some(first_embedded_primary_name),
|
|
Some(first_embedded_secondary_name),
|
|
first_embedded_tertiary_name,
|
|
embedded_name_tag_offsets.len()
|
|
),
|
|
format!(
|
|
"{} of {} embedded name rows use compact leading dword 0x{:08x}, matching the placed-structure embedded profile tag",
|
|
prefix_leading_dword_matching_embedded_profile_tag_count,
|
|
embedded_name_rows.len(),
|
|
u32::from(SAVE_REGION_RECORD_PROFILE_TAG)
|
|
),
|
|
format!(
|
|
"{decoded_embedded_name_row_count} decoded embedded name rows collapse into {} unique placed-structure name pairs; {} rows also expose a third embedded 0x55f1 string",
|
|
unique_embedded_name_pair_count
|
|
,
|
|
decoded_embedded_name_row_with_tertiary_name_count
|
|
),
|
|
dominant_compact_prefix_pattern
|
|
.map(|pattern| {
|
|
format!(
|
|
"dominant compact prefix pattern {} / {} / {} occurs {} times; section-like rows={}, cap-like rows={}, first names={:?}/{:?}",
|
|
pattern.prefix_leading_dword_hex,
|
|
pattern.prefix_trailing_word_hex,
|
|
pattern.prefix_separator_byte_hex,
|
|
pattern.count,
|
|
pattern.section_like_primary_name_count,
|
|
pattern.cap_like_primary_name_count,
|
|
pattern.first_primary_name,
|
|
pattern.first_secondary_name
|
|
)
|
|
})
|
|
.unwrap_or_else(|| {
|
|
"no dominant compact prefix pattern summary was available".to_string()
|
|
}),
|
|
],
|
|
});
|
|
}
|
|
None
|
|
}
|
|
|
|
fn summarize_placed_structure_dynamic_side_buffer_alignment(
|
|
side_buffer: &SmpSavePlacedStructureDynamicSideBufferProbe,
|
|
triplets: &SmpSavePlacedStructureRecordTripletProbe,
|
|
) -> SmpSavePlacedStructureDynamicSideBufferAlignmentProbe {
|
|
let triplet_name_pairs = triplets
|
|
.entries
|
|
.iter()
|
|
.map(|entry| (entry.primary_name.clone(), entry.secondary_name.clone()))
|
|
.collect::<BTreeSet<_>>();
|
|
let matched_name_pair_samples = side_buffer
|
|
.name_pair_summaries
|
|
.iter()
|
|
.filter(|summary| {
|
|
triplet_name_pairs
|
|
.contains(&(summary.primary_name.clone(), summary.secondary_name.clone()))
|
|
})
|
|
.take(5)
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
let unmatched_side_buffer_name_pair_samples = side_buffer
|
|
.name_pair_summaries
|
|
.iter()
|
|
.filter(|summary| {
|
|
!triplet_name_pairs
|
|
.contains(&(summary.primary_name.clone(), summary.secondary_name.clone()))
|
|
})
|
|
.take(5)
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
let side_buffer_rows_with_matching_triplet_name_pair_count = side_buffer
|
|
.name_pair_summaries
|
|
.iter()
|
|
.filter(|summary| {
|
|
triplet_name_pairs
|
|
.contains(&(summary.primary_name.clone(), summary.secondary_name.clone()))
|
|
})
|
|
.map(|summary| summary.count)
|
|
.sum::<usize>();
|
|
let unique_side_buffer_name_pair_count = side_buffer.name_pair_summaries.len();
|
|
let overlapping_name_pair_count = side_buffer
|
|
.name_pair_summaries
|
|
.iter()
|
|
.filter(|summary| {
|
|
triplet_name_pairs
|
|
.contains(&(summary.primary_name.clone(), summary.secondary_name.clone()))
|
|
})
|
|
.count();
|
|
SmpSavePlacedStructureDynamicSideBufferAlignmentProbe {
|
|
unique_side_buffer_name_pair_count,
|
|
unique_triplet_name_pair_count: triplet_name_pairs.len(),
|
|
overlapping_name_pair_count,
|
|
side_buffer_row_count: side_buffer.decoded_embedded_name_row_count,
|
|
side_buffer_rows_with_matching_triplet_name_pair_count,
|
|
side_buffer_rows_without_matching_triplet_name_pair_count: side_buffer
|
|
.decoded_embedded_name_row_count
|
|
.saturating_sub(side_buffer_rows_with_matching_triplet_name_pair_count),
|
|
triplet_name_pairs_without_side_buffer_match_count: triplet_name_pairs
|
|
.len()
|
|
.saturating_sub(overlapping_name_pair_count),
|
|
matched_name_pair_samples,
|
|
unmatched_side_buffer_name_pair_samples,
|
|
evidence: vec![
|
|
"placed-structure dynamic side-buffer alignment compares decoded 0x38a5 embedded name pairs against the grounded 0x36b1 triplet name-pair corpus".to_string(),
|
|
format!(
|
|
"{} of {} decoded side-buffer rows currently reuse name pairs already present in the placed-structure triplet owner seam",
|
|
side_buffer_rows_with_matching_triplet_name_pair_count,
|
|
side_buffer.decoded_embedded_name_row_count
|
|
),
|
|
format!(
|
|
"{} of {} unique side-buffer name pairs overlap the grounded triplet name-pair corpus",
|
|
overlapping_name_pair_count,
|
|
unique_side_buffer_name_pair_count
|
|
),
|
|
],
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct IndexedCollectionHeaderSummary {
|
|
metadata_tag_offset: usize,
|
|
records_tag_offset: usize,
|
|
close_tag_offset: usize,
|
|
direct_collection_flag: u32,
|
|
direct_record_stride: u32,
|
|
live_id_bound: u32,
|
|
live_record_count: u32,
|
|
header_words: [u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT],
|
|
}
|
|
|
|
fn parse_save_tagged_collection_header_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
metadata_tag: u32,
|
|
records_tag: u32,
|
|
close_tag: u32,
|
|
source_kind: &str,
|
|
semantic_family: &str,
|
|
predicate: impl Fn(IndexedCollectionHeaderSummary) -> bool,
|
|
mut evidence: Vec<String>,
|
|
) -> Option<SmpSaveTaggedCollectionHeaderProbe> {
|
|
if file_extension_hint != Some("gms") {
|
|
return None;
|
|
}
|
|
let profile = container_profile?;
|
|
if !matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
) {
|
|
return None;
|
|
}
|
|
|
|
let metadata_offsets = find_u32_le_offsets(bytes, metadata_tag);
|
|
let records_offsets = find_u32_le_offsets(bytes, records_tag);
|
|
let close_offsets = find_u32_le_offsets(bytes, close_tag);
|
|
|
|
let summary = metadata_offsets
|
|
.into_iter()
|
|
.filter_map(|metadata_tag_offset| {
|
|
let records_tag_offset = records_offsets
|
|
.iter()
|
|
.copied()
|
|
.find(|offset| *offset > metadata_tag_offset)?;
|
|
let close_tag_offset = close_offsets
|
|
.iter()
|
|
.copied()
|
|
.find(|offset| *offset > records_tag_offset)?;
|
|
let payload = bytes.get(metadata_tag_offset + 4..records_tag_offset)?;
|
|
if payload.len() < INDEXED_COLLECTION_SERIALIZED_HEADER_LEN {
|
|
return None;
|
|
}
|
|
|
|
let header_words = (0..INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT)
|
|
.map(|index| read_u32_at(payload, index * 4))
|
|
.collect::<Option<Vec<_>>>()?;
|
|
let header_words: [u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT] =
|
|
header_words.try_into().ok()?;
|
|
let summary = IndexedCollectionHeaderSummary {
|
|
metadata_tag_offset,
|
|
records_tag_offset,
|
|
close_tag_offset,
|
|
direct_collection_flag: header_words[0],
|
|
direct_record_stride: header_words[1],
|
|
live_id_bound: header_words[4],
|
|
live_record_count: header_words[5],
|
|
header_words,
|
|
};
|
|
predicate(summary).then_some(summary)
|
|
})
|
|
.next()?;
|
|
|
|
evidence.push(format!(
|
|
"exact little-endian u32 tag family 0x{metadata_tag:04x}/0x{records_tag:04x}/0x{close_tag:04x} appears at file offsets 0x{:x}/0x{:x}/0x{:x}",
|
|
summary.metadata_tag_offset, summary.records_tag_offset, summary.close_tag_offset
|
|
));
|
|
evidence.push(format!(
|
|
"header words report direct_collection_flag={}, direct_record_stride=0x{:x}, live_id_bound={}, live_record_count={}",
|
|
summary.direct_collection_flag,
|
|
summary.direct_record_stride,
|
|
summary.live_id_bound,
|
|
summary.live_record_count
|
|
));
|
|
|
|
Some(SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: profile.profile_family.clone(),
|
|
source_kind: source_kind.to_string(),
|
|
semantic_family: semantic_family.to_string(),
|
|
metadata_tag_offset: summary.metadata_tag_offset,
|
|
records_tag_offset: summary.records_tag_offset,
|
|
close_tag_offset: summary.close_tag_offset,
|
|
direct_collection_flag: summary.direct_collection_flag,
|
|
direct_collection_flag_hex: format!("0x{:08x}", summary.direct_collection_flag),
|
|
direct_record_stride: summary.direct_record_stride,
|
|
direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride),
|
|
live_id_bound: summary.live_id_bound,
|
|
live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound),
|
|
live_record_count: summary.live_record_count,
|
|
live_record_count_hex: format!("0x{:08x}", summary.live_record_count),
|
|
header_words: summary.header_words.to_vec(),
|
|
header_hex_words: summary
|
|
.header_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn scan_save_unclassified_tagged_collection_header_probes(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe> {
|
|
if file_extension_hint != Some("gms") {
|
|
return Vec::new();
|
|
}
|
|
let Some(profile) = container_profile else {
|
|
return Vec::new();
|
|
};
|
|
if !matches!(
|
|
profile.profile_family.as_str(),
|
|
"rt3-classic-save-container-v1"
|
|
| "rt3-105-save-container-v1"
|
|
| "rt3-105-scenario-save-container-v1"
|
|
| "rt3-105-alt-save-container-v1"
|
|
) {
|
|
return Vec::new();
|
|
}
|
|
let known_metadata_tags = BTreeSet::from([
|
|
RT3_SAVE_WORLD_BLOCK_CHUNK_TAG,
|
|
0x000061a9,
|
|
0x00005209,
|
|
0x000036b1,
|
|
EVENT_RUNTIME_COLLECTION_METADATA_TAG as u32,
|
|
]);
|
|
let mut low_tag_offsets: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
|
|
for offset in 0..bytes.len().saturating_sub(4) {
|
|
let Some(tag) = read_u32_at(bytes, offset) else {
|
|
continue;
|
|
};
|
|
if (3..=0xffff).contains(&tag) {
|
|
low_tag_offsets.entry(tag).or_default().push(offset);
|
|
}
|
|
}
|
|
let mut probes = Vec::new();
|
|
for (&metadata_tag, metadata_offsets) in &low_tag_offsets {
|
|
if known_metadata_tags.contains(&metadata_tag) {
|
|
continue;
|
|
}
|
|
let Some(records_offsets) = low_tag_offsets.get(&(metadata_tag + 1)) else {
|
|
continue;
|
|
};
|
|
let Some(close_offsets) = low_tag_offsets.get(&(metadata_tag + 2)) else {
|
|
continue;
|
|
};
|
|
let records_tag = metadata_tag + 1;
|
|
let close_tag = metadata_tag + 2;
|
|
for &metadata_tag_offset in metadata_offsets {
|
|
let mut header_words = [0u32; INDEXED_COLLECTION_SERIALIZED_HEADER_DWORD_COUNT];
|
|
let mut valid_header = true;
|
|
for (index, word) in header_words.iter_mut().enumerate() {
|
|
let Some(value) = read_u32_at(bytes, metadata_tag_offset + 4 + index * 4) else {
|
|
valid_header = false;
|
|
break;
|
|
};
|
|
*word = value;
|
|
}
|
|
if !valid_header {
|
|
continue;
|
|
}
|
|
let summary = IndexedCollectionHeaderSummary {
|
|
metadata_tag_offset,
|
|
records_tag_offset: 0,
|
|
close_tag_offset: 0,
|
|
direct_collection_flag: header_words[0],
|
|
direct_record_stride: header_words[1],
|
|
live_id_bound: header_words[4],
|
|
live_record_count: header_words[5],
|
|
header_words,
|
|
};
|
|
if !matches!(summary.direct_collection_flag, 0 | 1)
|
|
|| summary.direct_record_stride == 0
|
|
|| summary.direct_record_stride > 0x2000
|
|
|| summary.live_id_bound == 0
|
|
|| summary.live_record_count == 0
|
|
|| summary.live_record_count > summary.live_id_bound
|
|
|| summary.live_id_bound > 0x1000
|
|
|| summary.live_record_count > 0x1000
|
|
{
|
|
continue;
|
|
}
|
|
let records_search_start = metadata_tag_offset + 4;
|
|
let records_index =
|
|
records_offsets.partition_point(|offset| *offset < records_search_start);
|
|
let Some(&records_tag_offset) = records_offsets.get(records_index) else {
|
|
continue;
|
|
};
|
|
let close_search_start = records_tag_offset + 4;
|
|
let close_index = close_offsets.partition_point(|offset| *offset < close_search_start);
|
|
let Some(&close_tag_offset) = close_offsets.get(close_index) else {
|
|
continue;
|
|
};
|
|
let records_span_len = close_tag_offset.saturating_sub(records_tag_offset + 4);
|
|
if records_span_len == 0 || records_span_len < summary.live_record_count as usize {
|
|
continue;
|
|
}
|
|
if probes
|
|
.iter()
|
|
.any(|probe: &SmpSaveUnclassifiedTaggedCollectionHeaderProbe| {
|
|
probe.metadata_tag_offset == metadata_tag_offset
|
|
&& probe.records_tag_offset == records_tag_offset
|
|
&& probe.close_tag_offset == close_tag_offset
|
|
})
|
|
{
|
|
continue;
|
|
}
|
|
probes.push(SmpSaveUnclassifiedTaggedCollectionHeaderProbe {
|
|
profile_family: profile.profile_family.clone(),
|
|
source_kind: "save-unclassified-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-unclassified-tagged-header-counts".to_string(),
|
|
metadata_tag,
|
|
metadata_tag_hex: format!("0x{metadata_tag:08x}"),
|
|
records_tag,
|
|
records_tag_hex: format!("0x{records_tag:08x}"),
|
|
close_tag,
|
|
close_tag_hex: format!("0x{close_tag:08x}"),
|
|
metadata_tag_offset,
|
|
records_tag_offset,
|
|
close_tag_offset,
|
|
records_span_len,
|
|
direct_collection_flag: summary.direct_collection_flag,
|
|
direct_collection_flag_hex: format!("0x{:08x}", summary.direct_collection_flag),
|
|
direct_record_stride: summary.direct_record_stride,
|
|
direct_record_stride_hex: format!("0x{:08x}", summary.direct_record_stride),
|
|
live_id_bound: summary.live_id_bound,
|
|
live_id_bound_hex: format!("0x{:08x}", summary.live_id_bound),
|
|
live_record_count: summary.live_record_count,
|
|
live_record_count_hex: format!("0x{:08x}", summary.live_record_count),
|
|
header_words: summary.header_words.to_vec(),
|
|
header_hex_words: summary
|
|
.header_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect(),
|
|
evidence: vec![
|
|
"generic save-side tagged collection scan over plausible low u32 metadata tags not yet claimed by the checked-in collection probes".to_string(),
|
|
"candidate uses adjacent metadata/records/close tags with a header that matches the grounded indexed-collection shape (flag, stride, live_id_bound, live_record_count)".to_string(),
|
|
],
|
|
});
|
|
}
|
|
}
|
|
probes.sort_by(|left, right| {
|
|
right
|
|
.live_record_count
|
|
.cmp(&left.live_record_count)
|
|
.then_with(|| left.metadata_tag.cmp(&right.metadata_tag))
|
|
.then_with(|| left.metadata_tag_offset.cmp(&right.metadata_tag_offset))
|
|
});
|
|
probes.truncate(32);
|
|
probes
|
|
}
|
|
|
|
fn filter_unclassified_tagged_collection_header_probes_outside_known_spans(
|
|
probes: Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe>,
|
|
known_header_probes: &[Option<&SmpSaveTaggedCollectionHeaderProbe>],
|
|
) -> Vec<SmpSaveUnclassifiedTaggedCollectionHeaderProbe> {
|
|
probes
|
|
.into_iter()
|
|
.filter(|probe| {
|
|
!known_header_probes.iter().flatten().any(|known| {
|
|
probe.metadata_tag_offset >= known.metadata_tag_offset
|
|
&& probe.close_tag_offset <= known.close_tag_offset
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn parse_save_len_prefixed_ascii_name(bytes: &[u8]) -> Option<String> {
|
|
let len = *bytes.first()? as usize;
|
|
let text_bytes = bytes.get(1..1 + len)?;
|
|
let text = std::str::from_utf8(text_bytes).ok()?.trim_end_matches('\0');
|
|
Some(text.to_string())
|
|
}
|
|
|
|
fn parse_save_len_prefixed_ascii_name_pair(bytes: &[u8]) -> Option<(String, String)> {
|
|
let (first, second, _) = parse_save_len_prefixed_ascii_name_triplet(bytes)?;
|
|
Some((first, second))
|
|
}
|
|
|
|
fn parse_save_len_prefixed_ascii_name_triplet(
|
|
bytes: &[u8],
|
|
) -> Option<(String, String, Option<String>)> {
|
|
let first_len = *bytes.first()? as usize;
|
|
let first_end = 1 + first_len;
|
|
let first = std::str::from_utf8(bytes.get(1..first_end)?)
|
|
.ok()?
|
|
.trim_end_matches('\0')
|
|
.to_string();
|
|
let mut second_len_offset = first_end;
|
|
while matches!(bytes.get(second_len_offset), Some(0)) {
|
|
second_len_offset += 1;
|
|
}
|
|
let second_len = *bytes.get(second_len_offset)? as usize;
|
|
let second_start = second_len_offset + 1;
|
|
let second = std::str::from_utf8(bytes.get(second_start..second_start + second_len)?)
|
|
.ok()?
|
|
.trim_end_matches('\0')
|
|
.to_string();
|
|
if first.is_empty() || second.is_empty() {
|
|
return None;
|
|
}
|
|
let mut third_len_offset = second_start + second_len;
|
|
while matches!(bytes.get(third_len_offset), Some(0)) {
|
|
third_len_offset += 1;
|
|
}
|
|
let third = bytes
|
|
.get(third_len_offset)
|
|
.copied()
|
|
.filter(|len| *len != 0)
|
|
.and_then(|third_len| {
|
|
let third_len = third_len as usize;
|
|
let third_start = third_len_offset + 1;
|
|
let text = std::str::from_utf8(bytes.get(third_start..third_start + third_len)?)
|
|
.ok()?
|
|
.trim_end_matches('\0')
|
|
.to_string();
|
|
(!text.is_empty()).then_some(text)
|
|
});
|
|
Some((first, second, third))
|
|
}
|
|
|
|
fn parse_save_fixed_ascii_name(bytes: &[u8]) -> Option<String> {
|
|
let nul_index = bytes
|
|
.iter()
|
|
.position(|byte| *byte == 0)
|
|
.unwrap_or(bytes.len());
|
|
let text = std::str::from_utf8(bytes.get(..nul_index)?).ok()?;
|
|
if text.is_empty()
|
|
|| !text
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b' ' | b'-' | b'&' | b'/'))
|
|
{
|
|
return None;
|
|
}
|
|
Some(text.to_string())
|
|
}
|
|
|
|
fn parse_save_region_profile_collection_probe(
|
|
profile_payload: &[u8],
|
|
) -> Option<SmpSaveRegionProfileCollectionProbe> {
|
|
let direct_collection_flag = read_u32_at(profile_payload, 0)?;
|
|
let entry_stride = read_u32_at(profile_payload, 4)?;
|
|
let header_word_2 = read_u32_at(profile_payload, 8)?;
|
|
let header_word_3 = read_u32_at(profile_payload, 12)?;
|
|
let live_id_bound = read_u32_at(profile_payload, 16)?;
|
|
let live_record_count = read_u32_at(profile_payload, 20)?;
|
|
let header_word_6 = read_u32_at(profile_payload, 24)?;
|
|
let header_word_7 = read_u32_at(profile_payload, 28)?;
|
|
if !(direct_collection_flag == 1
|
|
&& entry_stride == 0x22
|
|
&& header_word_2 == 2
|
|
&& header_word_3 == 2
|
|
&& live_record_count > 0
|
|
&& live_record_count < live_id_bound
|
|
&& header_word_6 == 0
|
|
&& header_word_7 == 1)
|
|
{
|
|
return None;
|
|
}
|
|
let entry_stride = entry_stride as usize;
|
|
let live_record_count_usize = live_record_count as usize;
|
|
let rows_byte_len = live_record_count_usize.checked_mul(entry_stride)?;
|
|
let mut matched_probe = None;
|
|
for entry_start_relative_offset in 0x20..=0x80 {
|
|
if entry_start_relative_offset + rows_byte_len > profile_payload.len() {
|
|
break;
|
|
}
|
|
let mut entries = Vec::with_capacity(live_record_count_usize);
|
|
let mut matched = true;
|
|
for entry_index in 0..live_record_count_usize {
|
|
let row_relative_offset = entry_start_relative_offset + entry_index * entry_stride;
|
|
let row =
|
|
profile_payload.get(row_relative_offset..row_relative_offset + entry_stride)?;
|
|
let name = match parse_save_fixed_ascii_name(row.get(..12)?) {
|
|
Some(name) => name,
|
|
None => {
|
|
matched = false;
|
|
break;
|
|
}
|
|
};
|
|
let trailing_weight_f32 = f32::from_bits(read_u32_at(row, entry_stride - 4)?);
|
|
if !trailing_weight_f32.is_finite() || trailing_weight_f32 < 0.0 {
|
|
matched = false;
|
|
break;
|
|
}
|
|
entries.push(SmpSaveRegionProfileEntryProbe {
|
|
entry_index,
|
|
row_relative_offset,
|
|
name,
|
|
trailing_weight_f32,
|
|
});
|
|
}
|
|
if matched {
|
|
matched_probe = Some(SmpSaveRegionProfileCollectionProbe {
|
|
direct_collection_flag,
|
|
entry_stride: entry_stride as u32,
|
|
live_id_bound,
|
|
live_record_count,
|
|
entry_start_relative_offset,
|
|
trailing_padding_len: profile_payload.len()
|
|
- (entry_start_relative_offset + rows_byte_len),
|
|
entries,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
matched_probe
|
|
}
|
|
|
|
fn parse_rt3_105_save_name_table_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
bridge_payload_probe: Option<&SmpRt3105SaveBridgePayloadProbe>,
|
|
) -> Option<SmpRt3105SaveNameTableProbe> {
|
|
let (
|
|
profile_family,
|
|
source_kind,
|
|
header_offset,
|
|
entries_offset,
|
|
block_end_offset,
|
|
mut evidence,
|
|
) = if let Some(payload) = bridge_payload_probe {
|
|
(
|
|
payload.profile_family.clone(),
|
|
"save-bridge-secondary-block".to_string(),
|
|
payload.secondary_block_offset + 0x354,
|
|
payload.secondary_block_offset + 0x3b5,
|
|
payload.secondary_block_end_offset,
|
|
vec![
|
|
"common-save bridge payload branch".to_string(),
|
|
format!(
|
|
"secondary block span 0x{:x}..0x{:x}",
|
|
payload.secondary_block_offset, payload.secondary_block_end_offset
|
|
),
|
|
],
|
|
)
|
|
} else {
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
let extension = file_extension_hint.unwrap_or("");
|
|
let source_kind = match extension {
|
|
"gmp" => "map-fixed-catalog-range",
|
|
"gms" => "save-fixed-catalog-range",
|
|
"gmx" => "sandbox-fixed-catalog-range",
|
|
_ => "fixed-catalog-range",
|
|
}
|
|
.to_string();
|
|
(
|
|
profile_family,
|
|
source_kind,
|
|
0x6a70,
|
|
0x6ad1,
|
|
0x73c0,
|
|
vec![
|
|
"fixed catalog range branch".to_string(),
|
|
"using observed shared 1.05 candidate-availability table offsets".to_string(),
|
|
],
|
|
)
|
|
};
|
|
let entry_stride = 0x22usize;
|
|
if block_end_offset > bytes.len() {
|
|
return None;
|
|
}
|
|
if !matches_candidate_availability_table_header(bytes, header_offset) {
|
|
return None;
|
|
}
|
|
let observed_entry_capacity = read_u32_at(bytes, header_offset + 0x1c)? as usize;
|
|
let observed_entry_count = read_u32_at(bytes, header_offset + 0x20)? as usize;
|
|
let entries_len = observed_entry_count.checked_mul(entry_stride)?;
|
|
let entries_end_offset = entries_offset.checked_add(entries_len)?;
|
|
if observed_entry_count == 0 || observed_entry_capacity < observed_entry_count {
|
|
return None;
|
|
}
|
|
if entries_end_offset > block_end_offset || entries_end_offset > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let mut entries = Vec::with_capacity(observed_entry_count);
|
|
for index in 0..observed_entry_count {
|
|
let offset = entries_offset + index * entry_stride;
|
|
let chunk = &bytes[offset..offset + entry_stride];
|
|
let nul_index = chunk
|
|
.iter()
|
|
.position(|byte| *byte == 0)
|
|
.unwrap_or(entry_stride);
|
|
let text = std::str::from_utf8(&chunk[..nul_index]).ok()?.to_string();
|
|
let trailer_word = read_u32_at(bytes, offset + entry_stride - 4)?;
|
|
entries.push(SmpRt3105SaveNameTableEntry {
|
|
index,
|
|
offset,
|
|
text,
|
|
availability_dword: trailer_word,
|
|
availability_dword_hex: format!("0x{trailer_word:08x}"),
|
|
trailer_word,
|
|
trailer_word_hex: format!("0x{trailer_word:08x}"),
|
|
});
|
|
}
|
|
|
|
let zero_trailer_entry_names = entries
|
|
.iter()
|
|
.filter(|entry| entry.trailer_word == 0)
|
|
.map(|entry| entry.text.clone())
|
|
.collect::<Vec<_>>();
|
|
let zero_trailer_entry_count = zero_trailer_entry_names.len();
|
|
let nonzero_trailer_entry_count = entries.len().saturating_sub(zero_trailer_entry_count);
|
|
let mut distinct_trailer_words = entries
|
|
.iter()
|
|
.map(|entry| entry.trailer_word)
|
|
.collect::<Vec<_>>();
|
|
distinct_trailer_words.sort_unstable();
|
|
distinct_trailer_words.dedup();
|
|
let distinct_trailer_hex_words = distinct_trailer_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:08x}"))
|
|
.collect::<Vec<_>>();
|
|
let trailing_footer_hex = hex_encode(&bytes[entries_end_offset..block_end_offset]);
|
|
let footer = &bytes[entries_end_offset..block_end_offset];
|
|
if footer.len() != 9 {
|
|
return None;
|
|
}
|
|
let footer_progress_word_0 = u32::from_le_bytes([footer[0], footer[1], footer[2], footer[3]]);
|
|
let footer_progress_word_1 = u32::from_le_bytes([footer[4], footer[5], footer[6], footer[7]]);
|
|
let footer_trailing_byte = footer[8];
|
|
let mut footer_grounded_alignments = Vec::new();
|
|
for value in [footer_progress_word_0, footer_progress_word_1] {
|
|
if let Some(alignment) = classify_name_table_footer_progress_alignment(value) {
|
|
footer_grounded_alignments.push(alignment.to_string());
|
|
}
|
|
}
|
|
evidence.extend([
|
|
format!("header offset 0x{header_offset:08x}"),
|
|
format!("entries offset 0x{entries_offset:08x}"),
|
|
format!("entry stride 0x{entry_stride:x}"),
|
|
format!("observed entry capacity {}", observed_entry_capacity),
|
|
format!("observed entry count {}", observed_entry_count),
|
|
format!("zero-trailer entries {}", zero_trailer_entry_count),
|
|
format!(
|
|
"trailing footer {} bytes after last entry",
|
|
block_end_offset - entries_end_offset
|
|
),
|
|
]);
|
|
let semantic_alignment = vec![
|
|
"Matches the grounded scenario-side named candidate-availability table shape under 0x00437743.".to_string(),
|
|
"Entry layout matches 0x00434ea0/0x00434f20: name slot at +0x00..+0x1d and availability dword at +0x1e.".to_string(),
|
|
"The shared map/save range suggests this catalog is bundled in source map content and later mirrored into scenario state [state+0x66b2].".to_string(),
|
|
];
|
|
|
|
Some(SmpRt3105SaveNameTableProbe {
|
|
profile_family,
|
|
source_kind,
|
|
semantic_family: "scenario-named-candidate-availability-table".to_string(),
|
|
semantic_alignment,
|
|
header_offset,
|
|
header_word_0: read_u32_at(bytes, header_offset)?,
|
|
header_word_0_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset)?),
|
|
header_word_1: read_u32_at(bytes, header_offset + 4)?,
|
|
header_word_1_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset + 4)?),
|
|
header_word_2: read_u32_at(bytes, header_offset + 8)?,
|
|
header_word_2_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset + 8)?),
|
|
entry_stride,
|
|
entry_stride_hex: format!("0x{entry_stride:x}"),
|
|
header_prefix_word_count: 11,
|
|
observed_entry_capacity,
|
|
observed_entry_count,
|
|
zero_trailer_entry_count,
|
|
nonzero_trailer_entry_count,
|
|
distinct_trailer_words,
|
|
distinct_trailer_hex_words,
|
|
zero_trailer_entry_names,
|
|
entries_offset,
|
|
entries_end_offset,
|
|
trailing_footer_hex,
|
|
footer_progress_word_0,
|
|
footer_progress_word_0_hex: format!("0x{footer_progress_word_0:08x}"),
|
|
footer_progress_word_1,
|
|
footer_progress_word_1_hex: format!("0x{footer_progress_word_1:08x}"),
|
|
footer_trailing_byte,
|
|
footer_trailing_byte_hex: format!("0x{footer_trailing_byte:02x}"),
|
|
footer_grounded_alignments,
|
|
entries,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
const RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE: usize = 0x41;
|
|
const RT3_105_SAVE_NAMED_LOCOMOTIVE_MIN_ENTRY_COUNT: usize = 8;
|
|
const RT3_105_SAVE_NAMED_LOCOMOTIVE_MAX_SEARCH_SPAN: usize = 0x4000;
|
|
|
|
fn parse_rt3_105_save_named_locomotive_availability_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>,
|
|
) -> Option<SmpRt3105SaveNamedLocomotiveAvailabilityProbe> {
|
|
let packed_profile_probe = packed_profile_probe?;
|
|
let extension = file_extension_hint.unwrap_or("");
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| packed_profile_probe.profile_family.clone());
|
|
if !matches!(extension, "gms" | "gmx") || !profile_family.contains("save-container") {
|
|
return None;
|
|
}
|
|
|
|
let search_start = packed_profile_probe
|
|
.packed_profile_offset
|
|
.checked_add(packed_profile_probe.packed_profile_len)?;
|
|
let search_end = search_start
|
|
.checked_add(RT3_105_SAVE_NAMED_LOCOMOTIVE_MAX_SEARCH_SPAN)
|
|
.map(|end| end.min(bytes.len()))
|
|
.unwrap_or(bytes.len());
|
|
if search_end <= search_start + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE {
|
|
return None;
|
|
}
|
|
|
|
let mut best_start = None;
|
|
let mut best_entries = Vec::new();
|
|
for candidate_start in search_start..search_end {
|
|
let entries = parse_direct_named_locomotive_entries(bytes, candidate_start, search_end);
|
|
if entries.len() > best_entries.len() {
|
|
best_entries = entries;
|
|
best_start = Some(candidate_start);
|
|
}
|
|
}
|
|
|
|
if best_entries.len() < RT3_105_SAVE_NAMED_LOCOMOTIVE_MIN_ENTRY_COUNT {
|
|
return None;
|
|
}
|
|
|
|
let entries_offset = best_start?;
|
|
let entries_end_offset = entries_offset
|
|
.checked_add(best_entries.len() * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE)?;
|
|
let zero_availability_names = best_entries
|
|
.iter()
|
|
.filter(|entry| entry.availability_dword == 0)
|
|
.map(|entry| entry.text.clone())
|
|
.collect::<Vec<_>>();
|
|
let zero_availability_count = zero_availability_names.len();
|
|
let source_kind = match extension {
|
|
"gms" => "save-direct-locomotive-row-run",
|
|
"gmx" => "sandbox-direct-locomotive-row-run",
|
|
_ => "direct-locomotive-row-run",
|
|
}
|
|
.to_string();
|
|
|
|
let observed_entry_count = best_entries.len();
|
|
|
|
Some(SmpRt3105SaveNamedLocomotiveAvailabilityProbe {
|
|
profile_family,
|
|
source_kind,
|
|
semantic_family: "scenario-named-locomotive-availability-table".to_string(),
|
|
semantic_alignment: vec![
|
|
"Matches the grounded `.smp` save-side locomotive-name-plus-dword row family restored into scenario state [world+0x66b6].".to_string(),
|
|
"Entry layout is one availability dword at +0x00 followed by one fixed-width locomotive name buffer at +0x04..+0x40.".to_string(),
|
|
"The recovered row order is treated conservatively as the live locomotive ordinal order later used by locomotives-page descriptor lowering.".to_string(),
|
|
],
|
|
entries_offset,
|
|
entry_stride: RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE,
|
|
entry_stride_hex: format!("0x{:x}", RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE),
|
|
observed_entry_count,
|
|
zero_availability_count,
|
|
zero_availability_names,
|
|
entries_end_offset,
|
|
entries: best_entries,
|
|
evidence: vec![
|
|
format!("search span 0x{search_start:08x}..0x{search_end:08x}"),
|
|
format!("entries offset 0x{entries_offset:08x}"),
|
|
format!(
|
|
"entry stride 0x{:x}",
|
|
RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE
|
|
),
|
|
format!("observed entry count {observed_entry_count}"),
|
|
],
|
|
})
|
|
}
|
|
|
|
fn parse_direct_named_locomotive_entries(
|
|
bytes: &[u8],
|
|
start_offset: usize,
|
|
search_end: usize,
|
|
) -> Vec<SmpRt3105SaveNameTableEntry> {
|
|
let mut entries = Vec::new();
|
|
let mut offset = start_offset;
|
|
while offset + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE <= bytes.len() && offset < search_end
|
|
{
|
|
let record = &bytes[offset..offset + RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE];
|
|
let Some(nul_index) = record[4..].iter().position(|byte| *byte == 0) else {
|
|
break;
|
|
};
|
|
let name_bytes = &record[4..4 + nul_index];
|
|
if name_bytes.is_empty() {
|
|
break;
|
|
}
|
|
let Ok(text) = std::str::from_utf8(name_bytes) else {
|
|
break;
|
|
};
|
|
if !is_probable_named_locomotive_label(text) {
|
|
break;
|
|
}
|
|
if record[4 + nul_index + 1..].iter().any(|byte| *byte != 0) {
|
|
break;
|
|
}
|
|
|
|
let availability_dword = u32::from_le_bytes([record[0], record[1], record[2], record[3]]);
|
|
entries.push(SmpRt3105SaveNameTableEntry {
|
|
index: entries.len(),
|
|
offset,
|
|
text: text.to_string(),
|
|
availability_dword,
|
|
availability_dword_hex: format!("0x{availability_dword:08x}"),
|
|
trailer_word: availability_dword,
|
|
trailer_word_hex: format!("0x{availability_dword:08x}"),
|
|
});
|
|
offset += RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE;
|
|
}
|
|
entries
|
|
}
|
|
|
|
fn is_probable_named_locomotive_label(text: &str) -> bool {
|
|
if text.is_empty() || text.len() > 40 {
|
|
return false;
|
|
}
|
|
text.bytes().all(|byte| {
|
|
byte.is_ascii_alphanumeric() || matches!(byte, b' ' | b'-' | b'/' | b'(' | b')' | b'.')
|
|
})
|
|
}
|
|
|
|
fn parse_special_conditions_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
) -> Option<SmpSpecialConditionsProbe> {
|
|
let table_len = SPECIAL_CONDITION_COUNT.checked_mul(4)?;
|
|
let table_end = SPECIAL_CONDITIONS_OFFSET.checked_add(table_len)?;
|
|
if table_end > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let mut entries = Vec::with_capacity(SPECIAL_CONDITION_COUNT);
|
|
for definition in KNOWN_SPECIAL_CONDITION_DEFINITIONS {
|
|
let value = read_u32_at(
|
|
bytes,
|
|
SPECIAL_CONDITIONS_OFFSET + (definition.slot_index as usize) * 4,
|
|
)?;
|
|
if value > 1 {
|
|
return None;
|
|
}
|
|
entries.push(SmpSpecialConditionEntry {
|
|
slot_index: definition.slot_index,
|
|
hidden: definition.hidden,
|
|
label_id: definition.label_id,
|
|
help_id: definition.help_id,
|
|
label: definition.label.to_string(),
|
|
value,
|
|
value_hex: format!("0x{value:08x}"),
|
|
});
|
|
}
|
|
|
|
let hidden_sentinel = entries
|
|
.iter()
|
|
.find(|entry| entry.slot_index == SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT as u8)?;
|
|
if hidden_sentinel.value != 1 {
|
|
return None;
|
|
}
|
|
|
|
let enabled_visible_labels = entries
|
|
.iter()
|
|
.filter(|entry| !entry.hidden && entry.value != 0)
|
|
.map(|entry| entry.label.clone())
|
|
.collect::<Vec<_>>();
|
|
let source_kind = match file_extension_hint.unwrap_or("") {
|
|
"gmp" => "map-fixed-special-conditions-range",
|
|
"gms" => "save-fixed-special-conditions-range",
|
|
"gmx" => "sandbox-fixed-special-conditions-range",
|
|
_ => "fixed-special-conditions-range",
|
|
}
|
|
.to_string();
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
let mut evidence = vec![
|
|
format!("fixed 36-dword range at 0x{SPECIAL_CONDITIONS_OFFSET:04x}"),
|
|
"all observed lanes are boolean dwords".to_string(),
|
|
"hidden slot 35 carries the expected sentinel value 1".to_string(),
|
|
"slot metadata matches the grounded editor special-conditions table at 0x005f3ab0"
|
|
.to_string(),
|
|
];
|
|
if enabled_visible_labels.is_empty() {
|
|
evidence.push("no visible special conditions enabled in this file".to_string());
|
|
} else {
|
|
evidence.push(format!(
|
|
"enabled visible conditions: {}",
|
|
enabled_visible_labels.join(", ")
|
|
));
|
|
}
|
|
|
|
Some(SmpSpecialConditionsProbe {
|
|
profile_family,
|
|
source_kind,
|
|
table_offset: SPECIAL_CONDITIONS_OFFSET,
|
|
table_len,
|
|
enabled_visible_count: enabled_visible_labels.len(),
|
|
enabled_visible_labels,
|
|
hidden_sentinel_slot_index: SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT as u8,
|
|
hidden_sentinel_value: hidden_sentinel.value,
|
|
hidden_sentinel_value_hex: hidden_sentinel.value_hex.clone(),
|
|
entries,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn parse_post_special_conditions_scalar_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
special_conditions_probe: Option<&SmpSpecialConditionsProbe>,
|
|
) -> Option<SmpPostSpecialConditionsScalarProbe> {
|
|
special_conditions_probe?;
|
|
if POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let dword_count =
|
|
(POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET) / 4;
|
|
let mut nonzero_lanes = Vec::new();
|
|
for index in 0..dword_count {
|
|
let absolute_offset = POST_SPECIAL_CONDITIONS_SCALAR_OFFSET + index * 4;
|
|
let value = read_u32_at(bytes, absolute_offset)?;
|
|
if value == 0 {
|
|
continue;
|
|
}
|
|
nonzero_lanes.push(SmpPostSpecialConditionsScalarLane {
|
|
absolute_offset,
|
|
relative_offset: absolute_offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET,
|
|
absolute_offset_hex: format!("0x{absolute_offset:04x}"),
|
|
relative_offset_hex: format!(
|
|
"0x{:x}",
|
|
absolute_offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET
|
|
),
|
|
value,
|
|
value_hex: format!("0x{value:08x}"),
|
|
probable_f32_le: probable_normal_f32_string(value),
|
|
});
|
|
}
|
|
|
|
let source_kind = match file_extension_hint.unwrap_or("") {
|
|
"gmp" => "map-post-special-conditions-window",
|
|
"gms" => "save-post-special-conditions-window",
|
|
"gmx" => "sandbox-post-special-conditions-window",
|
|
_ => "post-special-conditions-window",
|
|
}
|
|
.to_string();
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
let first_nonzero_offset = nonzero_lanes.first().map(|lane| lane.absolute_offset);
|
|
let last_nonzero_offset = nonzero_lanes.last().map(|lane| lane.absolute_offset);
|
|
let overlap_nonzero_relative_offset_hexes = nonzero_lanes
|
|
.iter()
|
|
.filter(|lane| lane.absolute_offset < POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET)
|
|
.map(|lane| lane.relative_offset_hex.clone())
|
|
.collect::<Vec<_>>();
|
|
let tail_nonzero_lanes = nonzero_lanes
|
|
.iter()
|
|
.filter(|lane| lane.absolute_offset >= POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET)
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
let tail_first_nonzero_offset = tail_nonzero_lanes.first().map(|lane| lane.absolute_offset);
|
|
let tail_last_nonzero_offset = tail_nonzero_lanes.last().map(|lane| lane.absolute_offset);
|
|
let tail_nonzero_relative_offset_hexes = tail_nonzero_lanes
|
|
.iter()
|
|
.map(|lane| lane.relative_offset_hex.clone())
|
|
.collect::<Vec<_>>();
|
|
let grounded_text_field_remaining_file_window = &bytes[POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET
|
|
..POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET];
|
|
let mut grounded_text_field_remaining_nonzero_offsets = Vec::new();
|
|
for (index, byte) in grounded_text_field_remaining_file_window.iter().enumerate() {
|
|
if *byte != 0 {
|
|
grounded_text_field_remaining_nonzero_offsets
|
|
.push(POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET + index);
|
|
}
|
|
}
|
|
let grounded_text_field_remaining_first_nonzero_offset =
|
|
grounded_text_field_remaining_nonzero_offsets
|
|
.first()
|
|
.copied();
|
|
let grounded_text_field_remaining_last_nonzero_offset =
|
|
grounded_text_field_remaining_nonzero_offsets
|
|
.last()
|
|
.copied();
|
|
let mut evidence = vec![
|
|
format!(
|
|
"fixed post-sentinel dword window at 0x{POST_SPECIAL_CONDITIONS_SCALAR_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}"
|
|
),
|
|
"window starts immediately after the hidden special-conditions sentinel slot at 0x0df0"
|
|
.to_string(),
|
|
format!(
|
|
"leading overlap prefix 0x{POST_SPECIAL_CONDITIONS_SCALAR_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET:04x} aliases aligned runtime-rule band indices {}..{}",
|
|
SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX,
|
|
SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX
|
|
+ SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT
|
|
- 1
|
|
),
|
|
format!("save-only tail begins at 0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET:04x}"),
|
|
format!(
|
|
"that tail is offset-aligned with live runtime object bytes [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET:04x}..+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET:04x}]"
|
|
),
|
|
format!(
|
|
"the tail start lands on the grounded live field [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET:04x}], a 0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN:x}-byte status-text buffer written by win/lose and winner-announcement helpers"
|
|
),
|
|
format!(
|
|
"current dword scan stops at 0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}, leaving one byte-aligned continuation window 0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}..0x{POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET:04x} before the next clean live-field edge at [world+0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET:04x}]"
|
|
),
|
|
format!(
|
|
"the next exact grounded fields after that edge begin at [world+0x{POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET:04x}], [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}], and [world+0x{POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET:04x}], which map to file offsets 0x{POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET:04x}, 0x0f5d, 0x0f61, 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x}, and 0x0f6d"
|
|
),
|
|
format!(
|
|
"the first grounded dword-sized fields after that edge are [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}] and [world+0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET:04x}], which would land at file offsets 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x} and 0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET:04x}"
|
|
),
|
|
];
|
|
if nonzero_lanes.is_empty() {
|
|
evidence.push(
|
|
"all observed dwords in this post-sentinel window are zero for this file".to_string(),
|
|
);
|
|
} else {
|
|
evidence.push(format!(
|
|
"observed {} nonzero dword lanes between {} and {}",
|
|
nonzero_lanes.len(),
|
|
nonzero_lanes
|
|
.first()
|
|
.map(|lane| lane.absolute_offset_hex.as_str())
|
|
.unwrap_or("n/a"),
|
|
nonzero_lanes
|
|
.last()
|
|
.map(|lane| lane.absolute_offset_hex.as_str())
|
|
.unwrap_or("n/a")
|
|
));
|
|
if nonzero_lanes
|
|
.iter()
|
|
.all(|lane| lane.probable_f32_le.is_some())
|
|
{
|
|
evidence.push(
|
|
"every nonzero lane in this window also decodes as a normal finite little-endian f32"
|
|
.to_string(),
|
|
);
|
|
}
|
|
evidence.push(format!(
|
|
"{} nonzero lanes fall inside the aligned-band overlap prefix and {} fall inside the later tail",
|
|
overlap_nonzero_relative_offset_hexes.len(),
|
|
tail_nonzero_lanes.len()
|
|
));
|
|
}
|
|
evidence.push(
|
|
"checked file bytes in the later tail are not yet validated as a byte-for-byte mirror of the live object, because the region aligned to [world+0x4b47] does not currently decode as preserved text in the checked saves"
|
|
.to_string(),
|
|
);
|
|
if grounded_text_field_remaining_nonzero_offsets.is_empty() {
|
|
evidence.push(
|
|
"the remaining file window through the grounded text-field edge is all zero in this file"
|
|
.to_string(),
|
|
);
|
|
} else {
|
|
evidence.push(format!(
|
|
"the remaining file window through the grounded text-field edge still has {} nonzero bytes between 0x{:04x} and 0x{:04x}",
|
|
grounded_text_field_remaining_nonzero_offsets.len(),
|
|
grounded_text_field_remaining_first_nonzero_offset.unwrap_or(0),
|
|
grounded_text_field_remaining_last_nonzero_offset.unwrap_or(0)
|
|
));
|
|
}
|
|
|
|
Some(SmpPostSpecialConditionsScalarProbe {
|
|
profile_family,
|
|
source_kind,
|
|
window_offset: POST_SPECIAL_CONDITIONS_SCALAR_OFFSET,
|
|
window_end_offset: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET,
|
|
window_len: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET
|
|
- POST_SPECIAL_CONDITIONS_SCALAR_OFFSET,
|
|
window_len_hex: format!(
|
|
"0x{:x}",
|
|
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET
|
|
),
|
|
dword_count,
|
|
overlap_end_offset: POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET,
|
|
overlap_end_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET:04x}"
|
|
),
|
|
overlap_dword_count: (POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET
|
|
- POST_SPECIAL_CONDITIONS_SCALAR_OFFSET)
|
|
/ 4,
|
|
overlap_nonzero_dword_count: overlap_nonzero_relative_offset_hexes.len(),
|
|
overlap_nonzero_relative_offset_hexes,
|
|
tail_offset: POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET,
|
|
tail_offset_hex: format!("0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET:04x}"),
|
|
tail_len: POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET
|
|
- POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET,
|
|
tail_len_hex: format!(
|
|
"0x{:x}",
|
|
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET
|
|
),
|
|
tail_dword_count: (POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET
|
|
- POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET)
|
|
/ 4,
|
|
tail_runtime_object_offset: POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET,
|
|
tail_runtime_object_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET:04x}"
|
|
),
|
|
tail_runtime_object_end_offset:
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET,
|
|
tail_runtime_object_end_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET:04x}"
|
|
),
|
|
tail_runtime_object_validated_byte_mirror: false,
|
|
tail_grounded_live_field_offset:
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET,
|
|
tail_grounded_live_field_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET:04x}"
|
|
),
|
|
tail_grounded_live_field_name: "victory-or-outcome status text buffer".to_string(),
|
|
tail_grounded_live_field_copy_len:
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN,
|
|
tail_grounded_live_field_copy_len_hex: format!(
|
|
"0x{:x}",
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN
|
|
),
|
|
tail_grounded_live_field_copy_end_offset:
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET,
|
|
tail_grounded_live_field_copy_end_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET:04x}"
|
|
),
|
|
tail_window_cuts_through_grounded_live_field:
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET
|
|
< POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET,
|
|
tail_grounded_live_field_remaining_file_window_offset:
|
|
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET,
|
|
tail_grounded_live_field_remaining_file_window_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET:04x}"
|
|
),
|
|
tail_grounded_live_field_remaining_file_window_len:
|
|
POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET
|
|
- POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET,
|
|
tail_grounded_live_field_remaining_file_window_len_hex: format!(
|
|
"0x{:x}",
|
|
POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET
|
|
- POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET
|
|
),
|
|
tail_grounded_live_field_remaining_file_window_nonzero_byte_count:
|
|
grounded_text_field_remaining_nonzero_offsets.len(),
|
|
tail_grounded_live_field_remaining_file_window_first_nonzero_offset:
|
|
grounded_text_field_remaining_first_nonzero_offset,
|
|
tail_grounded_live_field_remaining_file_window_first_nonzero_offset_hex:
|
|
grounded_text_field_remaining_first_nonzero_offset
|
|
.map(|offset| format!("0x{offset:04x}")),
|
|
tail_grounded_live_field_remaining_file_window_last_nonzero_offset:
|
|
grounded_text_field_remaining_last_nonzero_offset,
|
|
tail_grounded_live_field_remaining_file_window_last_nonzero_offset_hex:
|
|
grounded_text_field_remaining_last_nonzero_offset
|
|
.map(|offset| format!("0x{offset:04x}")),
|
|
tail_next_grounded_dword_field_offset:
|
|
POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET,
|
|
tail_next_grounded_dword_field_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_OFFSET:04x}"
|
|
),
|
|
tail_next_grounded_dword_field_file_offset:
|
|
POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET,
|
|
tail_next_grounded_dword_field_file_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_0_FILE_OFFSET:04x}"
|
|
),
|
|
tail_second_grounded_dword_field_offset:
|
|
POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET,
|
|
tail_second_grounded_dword_field_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_OFFSET:04x}"
|
|
),
|
|
tail_second_grounded_dword_field_file_offset:
|
|
POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET,
|
|
tail_second_grounded_dword_field_file_offset_hex: format!(
|
|
"0x{POST_SPECIAL_CONDITIONS_NEXT_GROUNDED_DWORD_FIELD_1_FILE_OFFSET:04x}"
|
|
),
|
|
post_text_field_file_alignment_matches_grounded_dword_fields: false,
|
|
tail_nonzero_dword_count: tail_nonzero_lanes.len(),
|
|
tail_first_nonzero_offset,
|
|
tail_first_nonzero_offset_hex: tail_first_nonzero_offset
|
|
.map(|offset| format!("0x{offset:04x}")),
|
|
tail_last_nonzero_offset,
|
|
tail_last_nonzero_offset_hex: tail_last_nonzero_offset
|
|
.map(|offset| format!("0x{offset:04x}")),
|
|
tail_nonzero_relative_offset_hexes,
|
|
nonzero_dword_count: nonzero_lanes.len(),
|
|
first_nonzero_offset,
|
|
first_nonzero_offset_hex: first_nonzero_offset.map(|offset| format!("0x{offset:04x}")),
|
|
last_nonzero_offset,
|
|
last_nonzero_offset_hex: last_nonzero_offset.map(|offset| format!("0x{offset:04x}")),
|
|
nonzero_lanes,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn parse_post_text_field_neighborhood_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
special_conditions_probe: Option<&SmpSpecialConditionsProbe>,
|
|
) -> Option<SmpPostTextFieldNeighborhoodProbe> {
|
|
special_conditions_probe?;
|
|
if POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
let source_kind = match file_extension_hint.unwrap_or("") {
|
|
"gmp" => "post-text-grounded-field-neighborhood",
|
|
"gms" => "post-text-grounded-field-neighborhood",
|
|
"gmx" => "post-text-grounded-field-neighborhood",
|
|
_ => "post-text-grounded-field-neighborhood",
|
|
}
|
|
.to_string();
|
|
|
|
let exact_fields = [
|
|
(
|
|
"Auto-Show Grade During Track Lay",
|
|
POST_TEXT_FIELD_0_RUNTIME_OBJECT_OFFSET,
|
|
POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET,
|
|
1usize,
|
|
),
|
|
(
|
|
"Starting Building Density Level",
|
|
POST_TEXT_FIELD_1_RUNTIME_OBJECT_OFFSET,
|
|
0x0f5dusize,
|
|
1usize,
|
|
),
|
|
(
|
|
"Building Density Growth",
|
|
POST_TEXT_FIELD_2_RUNTIME_OBJECT_OFFSET,
|
|
0x0f61usize,
|
|
1usize,
|
|
),
|
|
(
|
|
"leftover simulation time accumulator",
|
|
POST_TEXT_FIELD_3_RUNTIME_OBJECT_OFFSET,
|
|
0x0f65usize,
|
|
4usize,
|
|
),
|
|
(
|
|
"selected-year lane snapshot",
|
|
POST_TEXT_FIELD_4_RUNTIME_OBJECT_OFFSET,
|
|
0x0f6dusize,
|
|
1usize,
|
|
),
|
|
(
|
|
"late locomotive policy gate dword",
|
|
POST_TEXT_FIELD_5_RUNTIME_OBJECT_OFFSET,
|
|
0x0f71usize,
|
|
4usize,
|
|
),
|
|
];
|
|
|
|
let grounded_field_observations = exact_fields
|
|
.iter()
|
|
.map(
|
|
|(field_name, runtime_object_offset, file_offset, field_width_bytes)| {
|
|
let raw = &bytes[*file_offset..*file_offset + *field_width_bytes];
|
|
let raw_hex = hex_encode(raw);
|
|
let (value_u8, value_u8_hex, value_u32, value_u32_hex, probable_f32_le) =
|
|
if *field_width_bytes == 1 {
|
|
let value = raw[0];
|
|
(
|
|
Some(value),
|
|
Some(format!("0x{value:02x}")),
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
} else {
|
|
let value = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
|
|
(
|
|
None,
|
|
None,
|
|
Some(value),
|
|
Some(format!("0x{value:08x}")),
|
|
probable_normal_f32_string(value),
|
|
)
|
|
};
|
|
SmpPostTextGroundedFieldObservation {
|
|
field_name: (*field_name).to_string(),
|
|
runtime_object_offset: *runtime_object_offset,
|
|
runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"),
|
|
file_offset: *file_offset,
|
|
file_offset_hex: format!("0x{file_offset:04x}"),
|
|
field_width_bytes: *field_width_bytes,
|
|
field_width_bytes_hex: format!("0x{field_width_bytes:x}"),
|
|
raw_hex,
|
|
value_u8,
|
|
value_u8_hex,
|
|
value_u32,
|
|
value_u32_hex,
|
|
probable_f32_le,
|
|
}
|
|
},
|
|
)
|
|
.collect::<Vec<_>>();
|
|
|
|
let one_byte_early_float_candidates = exact_fields
|
|
.iter()
|
|
.filter(|(_, _, file_offset, _)| *file_offset > 0)
|
|
.filter_map(|(field_name, runtime_object_offset, file_offset, _)| {
|
|
let candidate_offset = file_offset - 1;
|
|
let value = read_u32_at(bytes, candidate_offset)?;
|
|
let probable_f32_le = probable_normal_f32_string(value)?;
|
|
Some(SmpPostTextFloatAlignmentCandidate {
|
|
grounded_field_name: (*field_name).to_string(),
|
|
grounded_field_runtime_object_offset: *runtime_object_offset,
|
|
grounded_field_runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"),
|
|
grounded_field_file_offset: *file_offset,
|
|
grounded_field_file_offset_hex: format!("0x{file_offset:04x}"),
|
|
candidate_offset,
|
|
candidate_offset_hex: format!("0x{candidate_offset:04x}"),
|
|
candidate_value: value,
|
|
candidate_value_hex: format!("0x{value:08x}"),
|
|
probable_f32_le,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut evidence = vec![
|
|
format!(
|
|
"post-text grounded-field neighborhood spans file offsets 0x{POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET:04x}..0x{POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET:04x}"
|
|
),
|
|
"this neighborhood starts at the first grounded post-text field [world+0x4c74] and extends through the later dword at [world+0x4c8c]".to_string(),
|
|
"the exact grounded field offsets here are byte-oriented at 0x0f59, 0x0f5d, 0x0f61, and 0x0f6d, with dword-sized fields only at 0x0f65 and 0x0f71".to_string(),
|
|
];
|
|
if one_byte_early_float_candidates.is_empty() {
|
|
evidence.push(
|
|
"no one-byte-early little-endian float-looking starts were observed ahead of the grounded fields in this file".to_string(),
|
|
);
|
|
} else {
|
|
evidence.push(format!(
|
|
"observed {} float-looking 4-byte starts exactly one byte before grounded field offsets in this file",
|
|
one_byte_early_float_candidates.len()
|
|
));
|
|
}
|
|
|
|
Some(SmpPostTextFieldNeighborhoodProbe {
|
|
profile_family,
|
|
source_kind,
|
|
window_offset: POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET,
|
|
window_end_offset: POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET,
|
|
window_len: POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET - POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET,
|
|
window_len_hex: format!(
|
|
"0x{:x}",
|
|
POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET - POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET
|
|
),
|
|
grounded_field_observations,
|
|
one_byte_early_float_candidates,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn parse_locomotive_policy_neighborhood_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
special_conditions_probe: Option<&SmpSpecialConditionsProbe>,
|
|
) -> Option<SmpLocomotivePolicyNeighborhoodProbe> {
|
|
special_conditions_probe?;
|
|
if LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
let source_kind = match file_extension_hint.unwrap_or("") {
|
|
"gmp" => "locomotive-policy-neighborhood",
|
|
"gms" => "locomotive-policy-neighborhood",
|
|
"gmx" => "locomotive-policy-neighborhood",
|
|
_ => "locomotive-policy-neighborhood",
|
|
}
|
|
.to_string();
|
|
|
|
let exact_fields = [
|
|
(
|
|
"selected-year bucket companion scalar",
|
|
LOCOMOTIVE_POLICY_FIELD_NEG3_RUNTIME_OBJECT_OFFSET,
|
|
0x0f87usize,
|
|
4usize,
|
|
),
|
|
(
|
|
"startup-dispatch reset-owned band at +0x4cae",
|
|
LOCOMOTIVE_POLICY_FIELD_NEG2_RUNTIME_OBJECT_OFFSET,
|
|
0x0f93usize,
|
|
4usize,
|
|
),
|
|
(
|
|
"startup-dispatch reset-owned band at +0x4cb2",
|
|
LOCOMOTIVE_POLICY_FIELD_NEG1_RUNTIME_OBJECT_OFFSET,
|
|
0x0f97usize,
|
|
4usize,
|
|
),
|
|
(
|
|
"linked-site removal follow-on gate",
|
|
LOCOMOTIVE_POLICY_FIELD_0_RUNTIME_OBJECT_OFFSET,
|
|
0x0f78usize,
|
|
1usize,
|
|
),
|
|
(
|
|
"All Steam Locos Avail.",
|
|
LOCOMOTIVE_POLICY_FIELD_1_RUNTIME_OBJECT_OFFSET,
|
|
0x0f7cusize,
|
|
1usize,
|
|
),
|
|
(
|
|
"All Diesel Locos Avail.",
|
|
LOCOMOTIVE_POLICY_FIELD_2_RUNTIME_OBJECT_OFFSET,
|
|
0x0f7dusize,
|
|
1usize,
|
|
),
|
|
(
|
|
"All Electric Locos Avail.",
|
|
LOCOMOTIVE_POLICY_FIELD_3_RUNTIME_OBJECT_OFFSET,
|
|
0x0f7eusize,
|
|
1usize,
|
|
),
|
|
(
|
|
"station-list selected station id",
|
|
LOCOMOTIVE_POLICY_FIELD_4_RUNTIME_OBJECT_OFFSET,
|
|
0x0f9fusize,
|
|
4usize,
|
|
),
|
|
(
|
|
"cached available-locomotive rating",
|
|
LOCOMOTIVE_POLICY_FIELD_5_RUNTIME_OBJECT_OFFSET,
|
|
0x0fa3usize,
|
|
4usize,
|
|
),
|
|
];
|
|
|
|
let grounded_field_observations = exact_fields
|
|
.iter()
|
|
.map(
|
|
|(field_name, runtime_object_offset, file_offset, field_width_bytes)| {
|
|
let raw = &bytes[*file_offset..*file_offset + *field_width_bytes];
|
|
let raw_hex = hex_encode(raw);
|
|
let (value_u8, value_u8_hex, value_u32, value_u32_hex, probable_f32_le) =
|
|
if *field_width_bytes == 1 {
|
|
let value = raw[0];
|
|
(
|
|
Some(value),
|
|
Some(format!("0x{value:02x}")),
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
} else {
|
|
let value = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
|
|
(
|
|
None,
|
|
None,
|
|
Some(value),
|
|
Some(format!("0x{value:08x}")),
|
|
probable_normal_f32_string(value),
|
|
)
|
|
};
|
|
SmpLocomotivePolicyFieldObservation {
|
|
field_name: (*field_name).to_string(),
|
|
runtime_object_offset: *runtime_object_offset,
|
|
runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"),
|
|
file_offset: *file_offset,
|
|
file_offset_hex: format!("0x{file_offset:04x}"),
|
|
field_width_bytes: *field_width_bytes,
|
|
field_width_bytes_hex: format!("0x{field_width_bytes:x}"),
|
|
raw_hex,
|
|
value_u8,
|
|
value_u8_hex,
|
|
value_u32,
|
|
value_u32_hex,
|
|
probable_f32_le,
|
|
}
|
|
},
|
|
)
|
|
.collect::<Vec<_>>();
|
|
|
|
let three_byte_early_float_candidates = exact_fields
|
|
.iter()
|
|
.filter(|(_, _, _, width)| *width == 4usize)
|
|
.filter_map(|(field_name, runtime_object_offset, file_offset, _)| {
|
|
let candidate_offset = file_offset.saturating_sub(3);
|
|
let value = read_u32_at(bytes, candidate_offset)?;
|
|
let probable_f32_le = probable_normal_f32_string(value)?;
|
|
Some(SmpLocomotivePolicyFloatAlignmentCandidate {
|
|
grounded_field_name: (*field_name).to_string(),
|
|
grounded_field_runtime_object_offset: *runtime_object_offset,
|
|
grounded_field_runtime_object_offset_hex: format!("0x{runtime_object_offset:04x}"),
|
|
grounded_field_file_offset: *file_offset,
|
|
grounded_field_file_offset_hex: format!("0x{file_offset:04x}"),
|
|
candidate_offset,
|
|
candidate_offset_hex: format!("0x{candidate_offset:04x}"),
|
|
candidate_value: value,
|
|
candidate_value_hex: format!("0x{value:08x}"),
|
|
probable_f32_le,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut evidence = vec![
|
|
format!(
|
|
"locomotive-policy neighborhood spans file offsets 0x{LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET:04x}..0x{LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET:04x}"
|
|
),
|
|
"this neighborhood covers the selected-year bucket companion scalar, two startup-reset-owned bands, the linked-site removal gate, the three locomotive-availability policy bytes, the station-list selected-station mirror, and the cached available-locomotive rating".to_string(),
|
|
"the exact byte policy lanes live at 0x0f78 and 0x0f7c..0x0f7e, while the earlier grounded dword starts map to 0x0f87, 0x0f93, and 0x0f97 and the later grounded dword starts map to 0x0f9f and 0x0fa3".to_string(),
|
|
];
|
|
if three_byte_early_float_candidates.is_empty() {
|
|
evidence.push(
|
|
"no three-byte-early little-endian float-looking starts were observed ahead of the grounded dword fields in this file".to_string(),
|
|
);
|
|
} else {
|
|
evidence.push(format!(
|
|
"observed {} float-looking 4-byte starts exactly three bytes before grounded dword fields in this file",
|
|
three_byte_early_float_candidates.len()
|
|
));
|
|
}
|
|
|
|
Some(SmpLocomotivePolicyNeighborhoodProbe {
|
|
profile_family,
|
|
source_kind,
|
|
window_offset: LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET,
|
|
window_end_offset: LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET,
|
|
window_len: LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET
|
|
- LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET,
|
|
window_len_hex: format!(
|
|
"0x{:x}",
|
|
LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET - LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET
|
|
),
|
|
grounded_field_observations,
|
|
three_byte_early_float_candidates,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn parse_pre_recipe_scalar_plateau_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
special_conditions_probe: Option<&SmpSpecialConditionsProbe>,
|
|
) -> Option<SmpPreRecipeScalarPlateauProbe> {
|
|
special_conditions_probe?;
|
|
if PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
let source_kind = match file_extension_hint.unwrap_or("") {
|
|
"gmp" => "pre-recipe-scalar-plateau",
|
|
"gms" => "pre-recipe-scalar-plateau",
|
|
"gmx" => "pre-recipe-scalar-plateau",
|
|
_ => "pre-recipe-scalar-plateau",
|
|
}
|
|
.to_string();
|
|
|
|
let aligned_dword_count =
|
|
(PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET) / 4;
|
|
let mut nonzero_lanes = Vec::new();
|
|
for index in 0..aligned_dword_count {
|
|
let absolute_offset = PRE_RECIPE_SCALAR_PLATEAU_OFFSET + index * 4;
|
|
let value = read_u32_at(bytes, absolute_offset)?;
|
|
if value == 0 {
|
|
continue;
|
|
}
|
|
nonzero_lanes.push(SmpPreRecipeScalarPlateauLane {
|
|
absolute_offset,
|
|
relative_offset: absolute_offset - PRE_RECIPE_SCALAR_PLATEAU_OFFSET,
|
|
absolute_offset_hex: format!("0x{absolute_offset:04x}"),
|
|
relative_offset_hex: format!(
|
|
"0x{:x}",
|
|
absolute_offset - PRE_RECIPE_SCALAR_PLATEAU_OFFSET
|
|
),
|
|
value,
|
|
value_hex: format!("0x{value:08x}"),
|
|
probable_f32_le: probable_normal_f32_string(value),
|
|
});
|
|
}
|
|
|
|
let family_signature = match (
|
|
read_u32_at(bytes, 0x0faf),
|
|
read_u32_at(bytes, 0x0fb3),
|
|
read_u32_at(bytes, 0x0fcb),
|
|
) {
|
|
(Some(0x4000003f), Some(0xe560423f), Some(0x00000000)) => {
|
|
"rt3-105-scenario-pre-recipe-plateau-v1"
|
|
}
|
|
(Some(0x8000003f), Some(0x75c28f3f), Some(0x00300000)) => {
|
|
"rt3-105-base-pre-recipe-plateau-v1"
|
|
}
|
|
(Some(0x8000003f), Some(0x75c28f3f), Some(0xcdcdcd00)) => {
|
|
"rt3-105-alt-pre-recipe-plateau-v1"
|
|
}
|
|
_ => "unknown",
|
|
}
|
|
.to_string();
|
|
|
|
let mut evidence = vec![
|
|
format!(
|
|
"aligned scalar plateau spans file offsets 0x{PRE_RECIPE_SCALAR_PLATEAU_OFFSET:04x}..0x{PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET:04x}"
|
|
),
|
|
"this plateau ends immediately before the grounded recipe-book root at [world+0x0fe7]".to_string(),
|
|
"current grounding inside this span is still structural rather than semantic, so the probe only records aligned dword lanes and observed family signatures".to_string(),
|
|
];
|
|
if !nonzero_lanes.is_empty() {
|
|
evidence.push(format!(
|
|
"observed {} nonzero aligned dword lanes in the pre-recipe plateau",
|
|
nonzero_lanes.len()
|
|
));
|
|
}
|
|
if family_signature != "unknown" {
|
|
evidence.push(format!(
|
|
"matched observed family signature {family_signature}"
|
|
));
|
|
}
|
|
|
|
Some(SmpPreRecipeScalarPlateauProbe {
|
|
profile_family,
|
|
source_kind,
|
|
window_offset: PRE_RECIPE_SCALAR_PLATEAU_OFFSET,
|
|
window_end_offset: PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET,
|
|
window_len: PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET,
|
|
window_len_hex: format!(
|
|
"0x{:x}",
|
|
PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET - PRE_RECIPE_SCALAR_PLATEAU_OFFSET
|
|
),
|
|
aligned_dword_count,
|
|
family_signature,
|
|
nonzero_lanes,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn parse_recipe_book_summary_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
special_conditions_probe: Option<&SmpSpecialConditionsProbe>,
|
|
) -> Option<SmpRecipeBookSummaryProbe> {
|
|
special_conditions_probe?;
|
|
if RECIPE_BOOK_SUMMARY_END_OFFSET > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
let source_kind = match file_extension_hint.unwrap_or("") {
|
|
"gmp" => "recipe-book-summary",
|
|
"gms" => "recipe-book-summary",
|
|
"gmx" => "recipe-book-summary",
|
|
_ => "recipe-book-summary",
|
|
}
|
|
.to_string();
|
|
|
|
let mut books = Vec::with_capacity(RECIPE_BOOK_COUNT);
|
|
let mut mixed_head_count = 0usize;
|
|
let mut mixed_line_area_count = 0usize;
|
|
let mut cdcd_line_area_count = 0usize;
|
|
let mut zero_line_area_count = 0usize;
|
|
|
|
for book_index in 0..RECIPE_BOOK_COUNT {
|
|
let book_offset = RECIPE_BOOK_ROOT_OFFSET + book_index * RECIPE_BOOK_STRIDE;
|
|
let head = &bytes[book_offset..book_offset + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET];
|
|
let line_area_offset = book_offset + RECIPE_BOOK_LINE_AREA_OFFSET;
|
|
let line_area = &bytes[line_area_offset..line_area_offset + RECIPE_BOOK_LINE_AREA_LEN];
|
|
let max_annual_production_offset = book_offset + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET;
|
|
let max_annual_production_word = read_u32_at(bytes, max_annual_production_offset)?;
|
|
let mut lines = Vec::with_capacity(RECIPE_BOOK_LINE_COUNT);
|
|
for line_index in 0..RECIPE_BOOK_LINE_COUNT {
|
|
let line_offset = line_area_offset + line_index * RECIPE_BOOK_LINE_STRIDE;
|
|
let line = &bytes[line_offset..line_offset + RECIPE_BOOK_LINE_STRIDE];
|
|
let supplied_cargo_token_window = &line[0x08..0x20];
|
|
let demanded_cargo_token_window = &line[0x1c..0x30];
|
|
let mode_word = read_u32_at(bytes, line_offset)?;
|
|
let annual_amount_word = read_u32_at(bytes, line_offset + 0x04)?;
|
|
let supplied_cargo_token_word = read_u32_at(bytes, line_offset + 0x08)?;
|
|
let demanded_cargo_token_word = read_u32_at(bytes, line_offset + 0x1c)?;
|
|
lines.push(SmpRecipeBookLineSummary {
|
|
line_index,
|
|
line_offset,
|
|
line_offset_hex: format!("0x{line_offset:04x}"),
|
|
line_kind: classify_recipe_book_region_kind(line).to_string(),
|
|
line_signature_kind: classify_recipe_line_signature(
|
|
mode_word,
|
|
supplied_cargo_token_word,
|
|
demanded_cargo_token_word,
|
|
)
|
|
.to_string(),
|
|
imports_to_runtime_descriptor: mode_word != 0,
|
|
runtime_import_branch_kind: classify_recipe_runtime_import_branch(mode_word)
|
|
.to_string(),
|
|
line_nonzero_byte_count: line.iter().filter(|byte| **byte != 0).count(),
|
|
line_cdcd_byte_count: line.iter().filter(|byte| **byte == 0xcd).count(),
|
|
line_first_16_hex: hex_encode(&line[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(line.len())]),
|
|
mode_word_offset: line_offset,
|
|
mode_word_offset_hex: format!("0x{line_offset:04x}"),
|
|
mode_word,
|
|
mode_word_hex: format!("0x{mode_word:08x}"),
|
|
annual_amount_offset: line_offset + 0x04,
|
|
annual_amount_offset_hex: format!("0x{:04x}", line_offset + 0x04),
|
|
annual_amount_word,
|
|
annual_amount_word_hex: format!("0x{annual_amount_word:08x}"),
|
|
annual_amount_probable_f32_le: probable_normal_f32_string(annual_amount_word),
|
|
supplied_cargo_token_offset: line_offset + 0x08,
|
|
supplied_cargo_token_offset_hex: format!("0x{:04x}", line_offset + 0x08),
|
|
supplied_cargo_token_word,
|
|
supplied_cargo_token_word_hex: format!("0x{supplied_cargo_token_word:08x}"),
|
|
supplied_cargo_token_layout_kind: classify_recipe_token_layout(
|
|
supplied_cargo_token_word,
|
|
)
|
|
.to_string(),
|
|
supplied_cargo_token_window_hex: hex_encode(supplied_cargo_token_window),
|
|
supplied_cargo_token_window_ascii: ascii_preview(supplied_cargo_token_window),
|
|
supplied_cargo_token_active_in_runtime_import: mode_word != 0 && mode_word != 1,
|
|
supplied_cargo_token_probable_high16_ascii_stem:
|
|
probable_recipe_token_high16_ascii_stem(supplied_cargo_token_word),
|
|
demanded_cargo_token_offset: line_offset + 0x1c,
|
|
demanded_cargo_token_offset_hex: format!("0x{:04x}", line_offset + 0x1c),
|
|
demanded_cargo_token_word,
|
|
demanded_cargo_token_word_hex: format!("0x{demanded_cargo_token_word:08x}"),
|
|
demanded_cargo_token_layout_kind: classify_recipe_token_layout(
|
|
demanded_cargo_token_word,
|
|
)
|
|
.to_string(),
|
|
demanded_cargo_token_window_hex: hex_encode(demanded_cargo_token_window),
|
|
demanded_cargo_token_window_ascii: ascii_preview(demanded_cargo_token_window),
|
|
demanded_cargo_token_active_in_runtime_import: mode_word == 1 || mode_word == 3,
|
|
demanded_cargo_token_probable_high16_ascii_stem:
|
|
probable_recipe_token_high16_ascii_stem(demanded_cargo_token_word),
|
|
});
|
|
}
|
|
|
|
let head_kind = classify_recipe_book_region_kind(head).to_string();
|
|
let line_area_kind = classify_recipe_book_region_kind(line_area).to_string();
|
|
if head_kind == "mixed" {
|
|
mixed_head_count += 1;
|
|
}
|
|
match line_area_kind.as_str() {
|
|
"zero" => zero_line_area_count += 1,
|
|
"cdcd" => cdcd_line_area_count += 1,
|
|
_ => mixed_line_area_count += 1,
|
|
}
|
|
|
|
books.push(SmpRecipeBookSummaryBook {
|
|
book_index,
|
|
book_offset,
|
|
book_offset_hex: format!("0x{book_offset:04x}"),
|
|
head_kind,
|
|
head_nonzero_byte_count: head.iter().filter(|byte| **byte != 0).count(),
|
|
head_cdcd_byte_count: head.iter().filter(|byte| **byte == 0xcd).count(),
|
|
head_first_16_hex: hex_encode(&head[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(head.len())]),
|
|
max_annual_production_offset,
|
|
max_annual_production_offset_hex: format!("0x{max_annual_production_offset:04x}"),
|
|
max_annual_production_word,
|
|
max_annual_production_word_hex: format!("0x{max_annual_production_word:08x}"),
|
|
max_annual_production_probable_f32_le: probable_normal_f32_string(
|
|
max_annual_production_word,
|
|
),
|
|
line_area_offset,
|
|
line_area_offset_hex: format!("0x{line_area_offset:04x}"),
|
|
line_area_len: RECIPE_BOOK_LINE_AREA_LEN,
|
|
line_area_len_hex: format!("0x{:x}", RECIPE_BOOK_LINE_AREA_LEN),
|
|
line_area_kind,
|
|
line_area_nonzero_byte_count: line_area.iter().filter(|byte| **byte != 0).count(),
|
|
line_area_cdcd_byte_count: line_area.iter().filter(|byte| **byte == 0xcd).count(),
|
|
line_area_first_16_hex: hex_encode(
|
|
&line_area[..RECIPE_BOOK_HEAD_SAMPLE_LEN.min(line_area.len())],
|
|
),
|
|
lines,
|
|
});
|
|
}
|
|
|
|
let mut evidence = vec![
|
|
format!(
|
|
"grounded recipe-book root begins at file offset 0x{RECIPE_BOOK_ROOT_OFFSET:04x} and runtime offset [world+0x{RECIPE_BOOK_ROOT_OFFSET:04x}]"
|
|
),
|
|
format!(
|
|
"parsed {RECIPE_BOOK_COUNT} fixed books with stride 0x{RECIPE_BOOK_STRIDE:x}, shared cap lane at +0x{RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET:x}, and five line slots at +0x{RECIPE_BOOK_LINE_AREA_OFFSET:x} with stride 0x{RECIPE_BOOK_LINE_STRIDE:x}"
|
|
),
|
|
"this probe is structural only: it summarizes per-book heads plus five raw line records without decoding the mode or cargo-token semantics beyond the grounded offsets".to_string(),
|
|
];
|
|
evidence.push(format!(
|
|
"{mixed_head_count} books have mixed pre-line heads; line areas split into {zero_line_area_count} zero, {cdcd_line_area_count} cdcd, and {mixed_line_area_count} mixed books"
|
|
));
|
|
|
|
Some(SmpRecipeBookSummaryProbe {
|
|
profile_family,
|
|
source_kind,
|
|
root_offset: RECIPE_BOOK_ROOT_OFFSET,
|
|
root_offset_hex: format!("0x{RECIPE_BOOK_ROOT_OFFSET:04x}"),
|
|
runtime_object_root_offset: RECIPE_BOOK_ROOT_OFFSET,
|
|
runtime_object_root_offset_hex: format!("0x{RECIPE_BOOK_ROOT_OFFSET:04x}"),
|
|
book_count: RECIPE_BOOK_COUNT,
|
|
book_stride: RECIPE_BOOK_STRIDE,
|
|
book_stride_hex: format!("0x{:x}", RECIPE_BOOK_STRIDE),
|
|
max_annual_production_relative_offset: RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET,
|
|
max_annual_production_relative_offset_hex: format!(
|
|
"0x{:x}",
|
|
RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET
|
|
),
|
|
line_area_relative_offset: RECIPE_BOOK_LINE_AREA_OFFSET,
|
|
line_area_relative_offset_hex: format!("0x{:x}", RECIPE_BOOK_LINE_AREA_OFFSET),
|
|
line_count: RECIPE_BOOK_LINE_COUNT,
|
|
line_stride: RECIPE_BOOK_LINE_STRIDE,
|
|
line_stride_hex: format!("0x{:x}", RECIPE_BOOK_LINE_STRIDE),
|
|
books,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn parse_smp_aligned_runtime_rule_band_probe(
|
|
bytes: &[u8],
|
|
file_extension_hint: Option<&str>,
|
|
container_profile: Option<&SmpContainerProfile>,
|
|
special_conditions_probe: Option<&SmpSpecialConditionsProbe>,
|
|
) -> Option<SmpAlignedRuntimeRuleBandProbe> {
|
|
special_conditions_probe?;
|
|
if SMP_ALIGNED_RUNTIME_RULE_END_OFFSET > bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let source_kind = match file_extension_hint.unwrap_or("") {
|
|
"gmp" => "map-smp-aligned-runtime-rule-band",
|
|
"gms" => "save-smp-aligned-runtime-rule-band",
|
|
"gmx" => "sandbox-smp-aligned-runtime-rule-band",
|
|
_ => "smp-aligned-runtime-rule-band",
|
|
}
|
|
.to_string();
|
|
let profile_family = container_profile
|
|
.map(|profile| profile.profile_family.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
|
|
let mut nonzero_lanes = Vec::new();
|
|
for band_index in 0..SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT {
|
|
let absolute_offset = SPECIAL_CONDITIONS_OFFSET + band_index * 4;
|
|
let value = read_u32_at(bytes, absolute_offset)?;
|
|
if value == 0 {
|
|
continue;
|
|
}
|
|
let lane_kind = if band_index < SPECIAL_CONDITION_COUNT {
|
|
"known-special-condition-dword"
|
|
} else if band_index < SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT {
|
|
"unlabeled-editor-rule-dword"
|
|
} else {
|
|
"trailing-runtime-scalar"
|
|
}
|
|
.to_string();
|
|
let known_label = if band_index < SPECIAL_CONDITION_COUNT {
|
|
Some(
|
|
KNOWN_SPECIAL_CONDITION_DEFINITIONS[band_index]
|
|
.label
|
|
.to_string(),
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
nonzero_lanes.push(SmpAlignedRuntimeRuleBandLane {
|
|
band_index,
|
|
absolute_offset,
|
|
relative_offset: absolute_offset - SPECIAL_CONDITIONS_OFFSET,
|
|
absolute_offset_hex: format!("0x{absolute_offset:04x}"),
|
|
relative_offset_hex: format!("0x{:x}", absolute_offset - SPECIAL_CONDITIONS_OFFSET),
|
|
lane_kind,
|
|
known_label,
|
|
value,
|
|
value_hex: format!("0x{value:08x}"),
|
|
probable_f32_le: probable_normal_f32_string(value),
|
|
});
|
|
}
|
|
|
|
let nonzero_band_indices = nonzero_lanes
|
|
.iter()
|
|
.map(|lane| lane.band_index)
|
|
.collect::<Vec<_>>();
|
|
let nonzero_post_window_overlap_band_indices = nonzero_lanes
|
|
.iter()
|
|
.filter(|lane| {
|
|
lane.band_index >= SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX
|
|
&& lane.band_index
|
|
< SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX
|
|
+ SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT
|
|
})
|
|
.map(|lane| lane.band_index)
|
|
.collect::<Vec<_>>();
|
|
let nonzero_post_window_overlap_post_relative_offset_hexes = nonzero_lanes
|
|
.iter()
|
|
.filter(|lane| {
|
|
lane.band_index >= SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX
|
|
&& lane.band_index
|
|
< SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX
|
|
+ SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT
|
|
})
|
|
.map(|lane| {
|
|
format!(
|
|
"0x{:x}",
|
|
(lane.band_index - SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX) * 4
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let nonzero_relative_offset_hexes = nonzero_lanes
|
|
.iter()
|
|
.map(|lane| lane.relative_offset_hex.clone())
|
|
.collect::<Vec<_>>();
|
|
let mut evidence = vec![
|
|
format!(
|
|
"fixed `.smp`-aligned runtime-rule band at 0x{SPECIAL_CONDITIONS_OFFSET:04x}..0x{SMP_ALIGNED_RUNTIME_RULE_END_OFFSET:04x}"
|
|
),
|
|
format!(
|
|
"band spans {} known editor rule dwords plus one trailing scalar",
|
|
SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT
|
|
),
|
|
"first 36 dwords overlap the older fixed matrix probe rooted at 0x0d64".to_string(),
|
|
format!(
|
|
"trailing band indices {}..{} alias the leading post-sentinel window offsets {}..{}",
|
|
SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX,
|
|
SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX
|
|
+ SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT
|
|
- 1,
|
|
"0x00",
|
|
format!(
|
|
"0x{:x}",
|
|
(SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - 1) * 4
|
|
)
|
|
),
|
|
"band matches the grounded `.smp` save/load copy into `[world+0x4a7f..+0x4b43]`"
|
|
.to_string(),
|
|
];
|
|
if nonzero_lanes.is_empty() {
|
|
evidence
|
|
.push("all dwords in the aligned runtime-rule band are zero for this file".to_string());
|
|
} else {
|
|
evidence.push(format!(
|
|
"observed {} nonzero lanes at band indices {:?}",
|
|
nonzero_lanes.len(),
|
|
nonzero_band_indices
|
|
));
|
|
if !nonzero_post_window_overlap_band_indices.is_empty() {
|
|
evidence.push(format!(
|
|
"nonzero overlap lanes mirror post-window offsets {:?}",
|
|
nonzero_post_window_overlap_post_relative_offset_hexes
|
|
));
|
|
}
|
|
}
|
|
|
|
Some(SmpAlignedRuntimeRuleBandProbe {
|
|
profile_family,
|
|
source_kind,
|
|
band_offset: SPECIAL_CONDITIONS_OFFSET,
|
|
band_end_offset: SMP_ALIGNED_RUNTIME_RULE_END_OFFSET,
|
|
band_len: SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - SPECIAL_CONDITIONS_OFFSET,
|
|
band_len_hex: format!(
|
|
"0x{:x}",
|
|
SMP_ALIGNED_RUNTIME_RULE_END_OFFSET - SPECIAL_CONDITIONS_OFFSET
|
|
),
|
|
dword_count: SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT,
|
|
known_editor_rule_dword_count: SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT,
|
|
trailing_scalar_index: SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1,
|
|
trailing_scalar_offset: SPECIAL_CONDITIONS_OFFSET
|
|
+ (SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1) * 4,
|
|
trailing_scalar_offset_hex: format!(
|
|
"0x{:04x}",
|
|
SPECIAL_CONDITIONS_OFFSET + (SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT - 1) * 4
|
|
),
|
|
post_window_overlap_start_index: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX,
|
|
post_window_overlap_dword_count: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT,
|
|
post_window_overlap_end_index: SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX
|
|
+ SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT
|
|
- 1,
|
|
post_window_overlap_post_relative_offset_start_hex: "0x0".to_string(),
|
|
post_window_overlap_post_relative_offset_end_hex: format!(
|
|
"0x{:x}",
|
|
(SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT - 1) * 4
|
|
),
|
|
nonzero_post_window_overlap_band_indices,
|
|
nonzero_post_window_overlap_post_relative_offset_hexes,
|
|
nonzero_lane_count: nonzero_lanes.len(),
|
|
nonzero_band_indices,
|
|
nonzero_relative_offset_hexes,
|
|
nonzero_lanes,
|
|
evidence,
|
|
})
|
|
}
|
|
|
|
fn matches_candidate_availability_table_header(bytes: &[u8], header_offset: usize) -> bool {
|
|
matches!(
|
|
(
|
|
read_u32_at(bytes, header_offset + 0x08),
|
|
read_u32_at(bytes, header_offset + 0x0c),
|
|
read_u32_at(bytes, header_offset + 0x10),
|
|
read_u32_at(bytes, header_offset + 0x14),
|
|
read_u32_at(bytes, header_offset + 0x18),
|
|
read_u32_at(bytes, header_offset + 0x1c),
|
|
read_u32_at(bytes, header_offset + 0x20),
|
|
read_u32_at(bytes, header_offset + 0x24),
|
|
read_u32_at(bytes, header_offset + 0x28),
|
|
),
|
|
(
|
|
Some(0x0000332e),
|
|
Some(0x00000001),
|
|
Some(0x00000022),
|
|
Some(0x00000002),
|
|
Some(0x00000002),
|
|
Some(_),
|
|
Some(_),
|
|
Some(0x00000000),
|
|
Some(0x00000001),
|
|
)
|
|
)
|
|
}
|
|
|
|
fn classify_name_table_footer_progress_alignment(value: u32) -> Option<&'static str> {
|
|
match value {
|
|
0x32dc => Some(
|
|
"Footer progress word 0x000032dc matches the grounded late rehydrate progress id 0x32dc.",
|
|
),
|
|
0x3714 => Some(
|
|
"Footer progress word 0x00003714 matches the grounded late rehydrate progress id 0x3714.",
|
|
),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn parse_rt3_105_packed_profile_block(
|
|
bytes: &[u8],
|
|
packed_profile_offset: usize,
|
|
packed_profile_len: usize,
|
|
) -> Option<SmpRt3105PackedProfileBlock> {
|
|
let block_end = packed_profile_offset.checked_add(packed_profile_len)?;
|
|
if block_end > bytes.len() || packed_profile_len != 0x108 {
|
|
return None;
|
|
}
|
|
|
|
let leading_word_0 = read_u32_at(bytes, packed_profile_offset)?;
|
|
let trailing_zero_word_count_after_leading_word = (1..4)
|
|
.take_while(|index| {
|
|
read_u32_at(bytes, packed_profile_offset + (index * 4)).is_some_and(|word| word == 0)
|
|
})
|
|
.count();
|
|
let header_flag_word_3 = read_u32_at(bytes, packed_profile_offset + 0x0c)?;
|
|
let map_path_offset = 0x10usize;
|
|
let display_name_offset = 0x43usize;
|
|
let stable_nonzero_word_offsets = [0x00usize, 0x0c, 0x78, 0x7c, 0x80, 0x84];
|
|
let stable_nonzero_words = stable_nonzero_word_offsets
|
|
.iter()
|
|
.filter_map(|relative_offset| {
|
|
let value = read_u32_at(bytes, packed_profile_offset + relative_offset)?;
|
|
if value == 0 {
|
|
return None;
|
|
}
|
|
|
|
Some(SmpPackedProfileWordLane {
|
|
relative_offset: *relative_offset,
|
|
relative_offset_hex: format!("0x{relative_offset:02x}"),
|
|
value,
|
|
value_hex: format!("0x{value:08x}"),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
Some(SmpRt3105PackedProfileBlock {
|
|
relative_len: packed_profile_len,
|
|
relative_len_hex: format!("0x{packed_profile_len:03x}"),
|
|
leading_word_0,
|
|
leading_word_0_hex: format!("0x{leading_word_0:08x}"),
|
|
trailing_zero_word_count_after_leading_word,
|
|
header_flag_word_3,
|
|
header_flag_word_3_hex: format!("0x{header_flag_word_3:08x}"),
|
|
map_path_offset,
|
|
map_path: read_c_string_in_range(bytes, packed_profile_offset + map_path_offset, block_end),
|
|
display_name_offset,
|
|
display_name: read_c_string_in_range(
|
|
bytes,
|
|
packed_profile_offset + display_name_offset,
|
|
block_end,
|
|
),
|
|
profile_byte_0x77: bytes[packed_profile_offset + 0x77],
|
|
profile_byte_0x77_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x77]),
|
|
profile_byte_0x82: bytes[packed_profile_offset + 0x82],
|
|
profile_byte_0x82_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x82]),
|
|
profile_byte_0x97: bytes[packed_profile_offset + 0x97],
|
|
profile_byte_0x97_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x97]),
|
|
profile_byte_0xc5: bytes[packed_profile_offset + 0xc5],
|
|
profile_byte_0xc5_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0xc5]),
|
|
stable_nonzero_words,
|
|
})
|
|
}
|
|
|
|
fn collect_runtime_post_span_header_candidates(
|
|
bytes: &[u8],
|
|
start: usize,
|
|
search_len: usize,
|
|
) -> Vec<SmpRuntimePostSpanHeaderCandidate> {
|
|
let end = bytes.len().min(start + search_len);
|
|
let mut offset = start & !0x3;
|
|
let mut candidates = Vec::new();
|
|
|
|
while offset + 16 <= end && candidates.len() < 8 {
|
|
if let Some(candidate) = build_runtime_post_span_header_candidate(bytes, offset) {
|
|
let mut cluster_end = offset + 4;
|
|
while cluster_end + 16 <= end
|
|
&& build_runtime_post_span_header_candidate(bytes, cluster_end).is_some()
|
|
{
|
|
cluster_end += 4;
|
|
}
|
|
candidates.push(candidate);
|
|
offset = cluster_end;
|
|
} else {
|
|
offset += 4;
|
|
}
|
|
}
|
|
|
|
candidates
|
|
}
|
|
|
|
fn build_runtime_post_span_header_candidate(
|
|
bytes: &[u8],
|
|
offset: usize,
|
|
) -> Option<SmpRuntimePostSpanHeaderCandidate> {
|
|
let words = read_u32_window(bytes, offset, 4);
|
|
if words.len() < 4 {
|
|
return None;
|
|
}
|
|
|
|
let dense_words = words
|
|
.iter()
|
|
.copied()
|
|
.filter(|word| (word & 0xffff) == 0 && (word >> 16) != 0)
|
|
.collect::<Vec<_>>();
|
|
if dense_words.len() < 3 {
|
|
return None;
|
|
}
|
|
|
|
let high_u16_words = words
|
|
.iter()
|
|
.map(|word| (word >> 16) as u16)
|
|
.collect::<Vec<_>>();
|
|
let mut grounded_alignments = Vec::new();
|
|
for high in &high_u16_words {
|
|
if let Some(alignment) = classify_runtime_post_span_high16_grounded_alignment(*high) {
|
|
if !grounded_alignments
|
|
.iter()
|
|
.any(|existing| existing == alignment)
|
|
{
|
|
grounded_alignments.push(alignment.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(SmpRuntimePostSpanHeaderCandidate {
|
|
offset,
|
|
hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(),
|
|
dense_word_count: dense_words.len(),
|
|
high_hex_words: high_u16_words
|
|
.iter()
|
|
.map(|word| format!("0x{word:04x}"))
|
|
.collect(),
|
|
high_u16_words,
|
|
grounded_alignments,
|
|
words,
|
|
})
|
|
}
|
|
|
|
fn classify_runtime_post_span_high16_grounded_alignment(high_u16: u16) -> Option<&'static str> {
|
|
match high_u16 {
|
|
0x32dc => Some(
|
|
"High-16 value 0x32dc matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.",
|
|
),
|
|
0x3714 => Some(
|
|
"High-16 value 0x3714 matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.",
|
|
),
|
|
0x3715 => Some(
|
|
"High-16 value 0x3715 matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.",
|
|
),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn find_grounded_progress_high16_hits(
|
|
bytes: &[u8],
|
|
start: usize,
|
|
search_len: usize,
|
|
) -> Vec<String> {
|
|
let end = bytes.len().min(start + search_len);
|
|
let mut hits = Vec::new();
|
|
let mut offset = start & !0x3;
|
|
while offset + 4 <= end {
|
|
if let Some(word) = read_u32_at(bytes, offset) {
|
|
let high = (word >> 16) as u16;
|
|
if matches!(high, 0x32dc | 0x3714 | 0x3715) {
|
|
hits.push(format!("0x{high:04x}@0x{offset:08x}"));
|
|
}
|
|
}
|
|
offset += 4;
|
|
}
|
|
hits
|
|
}
|
|
|
|
fn parse_grounded_progress_hit_offset(hits: &[String], high_u16: u16) -> Option<usize> {
|
|
let needle = format!("0x{high_u16:04x}@0x");
|
|
let hit = hits.iter().find(|hit| hit.starts_with(&needle))?;
|
|
let offset_hex = hit.split("@0x").nth(1)?;
|
|
usize::from_str_radix(offset_hex, 16).ok()
|
|
}
|
|
|
|
fn collect_ascii_previews_in_range(
|
|
bytes: &[u8],
|
|
start: usize,
|
|
end: usize,
|
|
min_len: usize,
|
|
) -> Vec<SmpAsciiPreview> {
|
|
let mut previews = Vec::new();
|
|
let mut run_start = None;
|
|
let end = end.min(bytes.len());
|
|
|
|
for index in start..end {
|
|
let byte = bytes[index];
|
|
if is_ascii_preview_byte(byte) {
|
|
run_start.get_or_insert(index);
|
|
continue;
|
|
}
|
|
|
|
if let Some(current_start) = run_start.take() {
|
|
if index - current_start >= min_len {
|
|
previews.push(build_ascii_preview(bytes, current_start, index));
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(current_start) = run_start {
|
|
if end - current_start >= min_len {
|
|
previews.push(build_ascii_preview(bytes, current_start, end));
|
|
}
|
|
}
|
|
|
|
previews
|
|
}
|
|
|
|
fn find_c_string_with_suffix_in_range(
|
|
bytes: &[u8],
|
|
start: usize,
|
|
end: usize,
|
|
suffix: &str,
|
|
) -> Option<usize> {
|
|
let end = end.min(bytes.len());
|
|
let suffix = suffix.as_bytes();
|
|
let mut offset = start.min(end);
|
|
|
|
while offset < end {
|
|
if !is_ascii_preview_byte(bytes[offset]) {
|
|
offset += 1;
|
|
continue;
|
|
}
|
|
|
|
let run_start = offset;
|
|
while offset < end && is_ascii_preview_byte(bytes[offset]) {
|
|
offset += 1;
|
|
}
|
|
|
|
let run = &bytes[run_start..offset];
|
|
if run.ends_with(suffix) {
|
|
return Some(run_start);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn read_c_string_in_range(bytes: &[u8], start: usize, end: usize) -> Option<String> {
|
|
if start >= end || start >= bytes.len() {
|
|
return None;
|
|
}
|
|
|
|
let end = end.min(bytes.len());
|
|
let mut cursor = start;
|
|
while cursor < end && bytes[cursor] != 0 {
|
|
cursor += 1;
|
|
}
|
|
if cursor == start {
|
|
return None;
|
|
}
|
|
|
|
std::str::from_utf8(&bytes[start..cursor])
|
|
.ok()
|
|
.map(ToString::to_string)
|
|
}
|
|
|
|
fn find_u16_le_offsets(bytes: &[u8], needle: u16) -> Vec<usize> {
|
|
let pattern = needle.to_le_bytes();
|
|
bytes
|
|
.windows(pattern.len())
|
|
.enumerate()
|
|
.filter_map(|(offset, window)| (window == pattern).then_some(offset))
|
|
.collect()
|
|
}
|
|
|
|
fn find_u32_le_offsets(bytes: &[u8], needle: u32) -> Vec<usize> {
|
|
let pattern = needle.to_le_bytes();
|
|
bytes
|
|
.windows(pattern.len())
|
|
.enumerate()
|
|
.filter_map(|(offset, window)| (window == pattern).then_some(offset))
|
|
.collect()
|
|
}
|
|
|
|
fn find_next_nonzero_offset(bytes: &[u8], start: usize) -> Option<usize> {
|
|
bytes
|
|
.iter()
|
|
.enumerate()
|
|
.skip(start)
|
|
.find_map(|(offset, byte)| (*byte != 0).then_some(offset))
|
|
}
|
|
|
|
fn find_zero_run(bytes: &[u8], start: usize, min_len: usize) -> Option<usize> {
|
|
let mut run_start = None;
|
|
let mut run_len = 0usize;
|
|
|
|
for (offset, byte) in bytes.iter().enumerate().skip(start) {
|
|
if *byte == 0 {
|
|
run_start.get_or_insert(offset);
|
|
run_len += 1;
|
|
if run_len >= min_len {
|
|
return run_start;
|
|
}
|
|
} else {
|
|
run_start = None;
|
|
run_len = 0;
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn find_first_ascii_run(bytes: &[u8]) -> Option<SmpAsciiPreview> {
|
|
let mut start = None;
|
|
|
|
for (index, byte) in bytes.iter().copied().enumerate() {
|
|
if is_ascii_preview_byte(byte) {
|
|
start.get_or_insert(index);
|
|
continue;
|
|
}
|
|
|
|
if let Some(run_start) = start.take() {
|
|
if index - run_start >= MIN_ASCII_RUN_LEN {
|
|
return Some(build_ascii_preview(bytes, run_start, index));
|
|
}
|
|
}
|
|
}
|
|
|
|
start.and_then(|run_start| {
|
|
if bytes.len() - run_start >= MIN_ASCII_RUN_LEN {
|
|
Some(build_ascii_preview(bytes, run_start, bytes.len()))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn build_ascii_preview(bytes: &[u8], start: usize, end: usize) -> SmpAsciiPreview {
|
|
let byte_len = end - start;
|
|
let preview_bytes = &bytes[start..end];
|
|
let preview = String::from_utf8_lossy(
|
|
&preview_bytes[..preview_bytes.len().min(ASCII_PREVIEW_CHAR_LIMIT)],
|
|
)
|
|
.into_owned();
|
|
|
|
SmpAsciiPreview {
|
|
offset: start,
|
|
byte_len,
|
|
truncated: byte_len > ASCII_PREVIEW_CHAR_LIMIT,
|
|
preview,
|
|
}
|
|
}
|
|
|
|
fn is_ascii_preview_byte(byte: u8) -> bool {
|
|
matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x21..=0x7e)
|
|
}
|
|
|
|
fn read_u32_window(bytes: &[u8], offset: usize, count: usize) -> Vec<u32> {
|
|
let mut words = Vec::new();
|
|
let end = bytes.len().min(offset + count * 4);
|
|
for chunk in bytes[offset..end].chunks_exact(4) {
|
|
words.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
|
|
}
|
|
words
|
|
}
|
|
|
|
fn read_u8_at(bytes: &[u8], offset: usize) -> Option<u8> {
|
|
bytes.get(offset).copied()
|
|
}
|
|
|
|
fn read_u16_at(bytes: &[u8], offset: usize) -> Option<u16> {
|
|
let chunk = bytes.get(offset..offset + 2)?;
|
|
Some(u16::from_le_bytes([chunk[0], chunk[1]]))
|
|
}
|
|
|
|
fn read_u32_at(bytes: &[u8], offset: usize) -> Option<u32> {
|
|
let chunk = bytes.get(offset..offset + 4)?;
|
|
Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
|
}
|
|
|
|
fn read_i32_at(bytes: &[u8], offset: usize) -> Option<i32> {
|
|
let chunk = bytes.get(offset..offset + 4)?;
|
|
Some(i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
|
}
|
|
|
|
fn read_i64_at(bytes: &[u8], offset: usize) -> Option<i64> {
|
|
let chunk = bytes.get(offset..offset + 8)?;
|
|
Some(i64::from_le_bytes([
|
|
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
|
|
]))
|
|
}
|
|
|
|
fn read_u64_at(bytes: &[u8], offset: usize) -> Option<u64> {
|
|
let chunk = bytes.get(offset..offset + 8)?;
|
|
Some(u64::from_le_bytes([
|
|
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
|
|
]))
|
|
}
|
|
|
|
fn read_f32_at(bytes: &[u8], offset: usize) -> Option<f32> {
|
|
let chunk = bytes.get(offset..offset + 4)?;
|
|
Some(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
|
}
|
|
|
|
fn read_f64_at(bytes: &[u8], offset: usize) -> Option<f64> {
|
|
let chunk = bytes.get(offset..offset + 8)?;
|
|
Some(f64::from_le_bytes([
|
|
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
|
|
]))
|
|
}
|
|
|
|
fn read_ascii_c_string_at(bytes: &[u8], offset: usize, max_len: usize) -> Option<String> {
|
|
let chunk = bytes.get(offset..offset + max_len)?;
|
|
let nul_index = chunk.iter().position(|byte| *byte == 0).unwrap_or(max_len);
|
|
let text = std::str::from_utf8(&chunk[..nul_index])
|
|
.ok()?
|
|
.trim()
|
|
.to_string();
|
|
Some(text)
|
|
}
|
|
|
|
fn parse_nonzero_u32(bytes: &[u8], offset: usize) -> Option<Option<u32>> {
|
|
read_u32_at(bytes, offset).map(|value| (value != 0).then_some(value))
|
|
}
|
|
|
|
fn round_f64_to_i64(value: f64) -> Option<i64> {
|
|
if !value.is_finite() {
|
|
return None;
|
|
}
|
|
let rounded = value.round();
|
|
if rounded < i64::MIN as f64 || rounded > i64::MAX as f64 {
|
|
return None;
|
|
}
|
|
Some(rounded as i64)
|
|
}
|
|
|
|
fn probable_normal_f32_string(value: u32) -> Option<String> {
|
|
let exponent = (value >> 23) & 0xff;
|
|
if exponent == 0 || exponent == 0xff {
|
|
return None;
|
|
}
|
|
let scalar = f32::from_bits(value);
|
|
if !scalar.is_finite() {
|
|
return None;
|
|
}
|
|
Some(format!("{scalar:.6}"))
|
|
}
|
|
|
|
fn probable_recipe_token_high16_ascii_stem(value: u32) -> Option<String> {
|
|
if value & 0xffff != 0 {
|
|
return None;
|
|
}
|
|
let high = ((value >> 16) & 0xffff) as u16;
|
|
if high == 0 {
|
|
return None;
|
|
}
|
|
let low_byte = (high & 0x00ff) as u8;
|
|
let high_byte = (high >> 8) as u8;
|
|
if !low_byte.is_ascii_alphabetic() || !high_byte.is_ascii_alphabetic() {
|
|
return None;
|
|
}
|
|
Some(format!("{}{}", low_byte as char, high_byte as char))
|
|
}
|
|
|
|
fn classify_recipe_token_layout(value: u32) -> &'static str {
|
|
if value == 0 {
|
|
return "zero";
|
|
}
|
|
if probable_recipe_token_high16_ascii_stem(value).is_some() {
|
|
return "high16-ascii-stem";
|
|
}
|
|
if value & 0xffff == 0 {
|
|
return "high16-numeric";
|
|
}
|
|
if value >> 16 == 0 {
|
|
return "low16-marker";
|
|
}
|
|
"mixed"
|
|
}
|
|
|
|
fn classify_recipe_line_signature(
|
|
mode_word: u32,
|
|
supplied_cargo_token_word: u32,
|
|
demanded_cargo_token_word: u32,
|
|
) -> &'static str {
|
|
let supplied_layout = classify_recipe_token_layout(supplied_cargo_token_word);
|
|
let demanded_layout = classify_recipe_token_layout(demanded_cargo_token_word);
|
|
if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_layout == "high16-numeric" {
|
|
return "demand-numeric-entry";
|
|
}
|
|
if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_layout == "high16-ascii-stem" {
|
|
return "demand-stem-entry";
|
|
}
|
|
if mode_word == 0 && demanded_cargo_token_word == 0 && supplied_layout == "high16-numeric" {
|
|
return "supply-numeric-entry";
|
|
}
|
|
if mode_word != 0 && demanded_cargo_token_word == 0 && supplied_layout == "low16-marker" {
|
|
return "supply-marker-entry";
|
|
}
|
|
if mode_word == 0 && supplied_cargo_token_word == 0 && demanded_cargo_token_word == 0 {
|
|
return "zero";
|
|
}
|
|
"mixed"
|
|
}
|
|
|
|
fn classify_recipe_runtime_import_branch(mode_word: u32) -> &'static str {
|
|
if mode_word == 0 {
|
|
return "zero-mode-skipped";
|
|
}
|
|
if mode_word == 1 {
|
|
return "mode1-demand-branch";
|
|
}
|
|
if mode_word == 3 {
|
|
return "mode3-dual-branch";
|
|
}
|
|
"nonzero-supply-branch"
|
|
}
|
|
|
|
fn classify_recipe_book_region_kind(bytes: &[u8]) -> &'static str {
|
|
if bytes.iter().all(|byte| *byte == 0) {
|
|
"zero"
|
|
} else if bytes.iter().all(|byte| *byte == 0xcd) {
|
|
"cdcd"
|
|
} else {
|
|
"mixed"
|
|
}
|
|
}
|
|
|
|
fn hex_encode(bytes: &[u8]) -> String {
|
|
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
|
}
|
|
|
|
fn ascii_preview(bytes: &[u8]) -> String {
|
|
bytes
|
|
.iter()
|
|
.map(|byte| match byte {
|
|
0x20..=0x7e => char::from(*byte),
|
|
_ => '.',
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn sha256_hex(bytes: &[u8]) -> String {
|
|
let digest = Sha256::digest(bytes);
|
|
format!("{digest:x}")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn reports_grounded_tag_hits_and_offsets() {
|
|
let bytes = [
|
|
0x34, 0x12, 0x00, 0x00, 0xe0, 0x2e, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x80,
|
|
0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x71, 0x07, 0x00, 0x00, 0x71, 0x07, 0x00, 0x00,
|
|
0x71, 0x07, 0x00, 0x00, 0xaa, 0xbb, 0x00, 0x00, 0xcc, 0xdd, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, b'H', b'e', b'l',
|
|
b'l', b'o', b' ', b'R', b'R', b'T', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc, 0xde, 0xf0, 0x00, 0x00, 0x00, 0x00, 0xee, 0x2c, 0x11, 0x51, 0x2d, 0x22,
|
|
0x71, 0x94, 0x33, 0x72, 0x94,
|
|
];
|
|
let report = inspect_smp_bytes(&bytes);
|
|
|
|
assert!(report.contains_grounded_runtime_tags);
|
|
assert_eq!(report.known_tag_hits.len(), 4);
|
|
assert_eq!(report.preamble.word_count, 16);
|
|
assert_eq!(report.preamble.words[0].value_le, 0x00001234);
|
|
let shared_header = report
|
|
.shared_header
|
|
.as_ref()
|
|
.expect("shared header should parse");
|
|
assert!(shared_header.matches_grounded_common_signature);
|
|
let header_variant = report
|
|
.header_variant_probe
|
|
.as_ref()
|
|
.expect("header variant probe should exist");
|
|
assert_eq!(header_variant.variant_family, "unknown");
|
|
assert!(!header_variant.is_known_family);
|
|
assert_eq!(shared_header.primary_family_tag, 0x00002ee0);
|
|
assert_eq!(
|
|
shared_header.payload_window_words_8_to_9,
|
|
vec![0x0000bbaa, 0x0000ddcc]
|
|
);
|
|
assert!(shared_header.reserved_words_10_to_14_all_zero);
|
|
assert_eq!(shared_header.final_flag_word, 0);
|
|
let ascii_run = report
|
|
.first_ascii_run
|
|
.as_ref()
|
|
.expect("ascii run should exist");
|
|
assert_eq!(ascii_run.offset, 67);
|
|
assert_eq!(ascii_run.byte_len, 9);
|
|
assert_eq!(ascii_run.preview, "Hello RRT");
|
|
let early_probe = report
|
|
.early_content_probe
|
|
.as_ref()
|
|
.expect("early content probe should exist");
|
|
assert_eq!(early_probe.first_post_text_nonzero_offset, 88);
|
|
assert_eq!(early_probe.zero_pad_after_text_len, 12);
|
|
assert_eq!(early_probe.first_post_text_block_len, 4);
|
|
assert_eq!(early_probe.first_post_text_block_hex, "11223344");
|
|
assert_eq!(early_probe.trailing_zero_pad_after_first_block_len, 16);
|
|
assert_eq!(early_probe.secondary_nonzero_offset, Some(108));
|
|
assert_eq!(early_probe.secondary_aligned_word_window_offset, Some(108));
|
|
assert_eq!(
|
|
&early_probe.secondary_aligned_word_window_words[..2],
|
|
&[0x78563412, 0xf0debc9a]
|
|
);
|
|
assert!(
|
|
early_probe
|
|
.secondary_preview_hex
|
|
.starts_with("123456789abcdef0")
|
|
);
|
|
let secondary_variant = report
|
|
.secondary_variant_probe
|
|
.as_ref()
|
|
.expect("secondary variant probe should exist");
|
|
assert_eq!(secondary_variant.variant_family, "unknown");
|
|
let container_profile = report
|
|
.container_profile
|
|
.as_ref()
|
|
.expect("container profile should exist");
|
|
assert_eq!(container_profile.profile_family, "unknown");
|
|
assert!(!container_profile.is_known_profile);
|
|
assert!(report.save_bootstrap_block.is_none());
|
|
assert!(report.save_anchor_run_block.is_none());
|
|
assert!(report.runtime_anchor_cycle_block.is_none());
|
|
assert!(report.runtime_trailer_block.is_none());
|
|
assert!(report.runtime_post_span_probe.is_none());
|
|
assert!(report.classic_rehydrate_profile_probe.is_none());
|
|
assert_eq!(report.known_tag_hits[0].tag_id, 0x2cee);
|
|
assert_eq!(report.known_tag_hits[0].hit_count, 1);
|
|
assert_eq!(report.known_tag_hits[0].sample_offsets, vec![120]);
|
|
assert_eq!(report.known_tag_hits[1].tag_id, 0x2d51);
|
|
assert_eq!(report.known_tag_hits[1].sample_offsets, vec![123]);
|
|
assert_eq!(report.known_tag_hits[2].tag_id, 0x9471);
|
|
assert_eq!(report.known_tag_hits[2].sample_offsets, vec![126]);
|
|
assert_eq!(report.known_tag_hits[3].tag_id, 0x9472);
|
|
assert_eq!(report.known_tag_hits[3].sample_offsets, vec![129]);
|
|
}
|
|
|
|
#[test]
|
|
fn warns_when_no_grounded_tags_are_present() {
|
|
let report = inspect_smp_bytes(&[0xaa, 0xbb, 0xcc]);
|
|
|
|
assert!(!report.contains_grounded_runtime_tags);
|
|
assert!(report.known_tag_hits.is_empty());
|
|
assert_eq!(report.preamble.word_count, 0);
|
|
assert!(report.shared_header.is_none());
|
|
assert!(report.header_variant_probe.is_none());
|
|
assert!(report.first_ascii_run.is_none());
|
|
assert!(report.early_content_probe.is_none());
|
|
assert!(report.secondary_variant_probe.is_none());
|
|
assert!(report.container_profile.is_none());
|
|
assert!(report.save_bootstrap_block.is_none());
|
|
assert!(report.save_anchor_run_block.is_none());
|
|
assert!(report.runtime_anchor_cycle_block.is_none());
|
|
assert!(report.runtime_trailer_block.is_none());
|
|
assert!(report.runtime_post_span_probe.is_none());
|
|
assert!(report.classic_rehydrate_profile_probe.is_none());
|
|
assert!(
|
|
report
|
|
.warnings
|
|
.iter()
|
|
.any(|warning| warning.contains("No grounded runtime bundle tags were found"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_zeroed_post_special_conditions_scalar_window() {
|
|
let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET];
|
|
let sentinel_offset =
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4;
|
|
bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
|
|
let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None)
|
|
.expect("special-conditions probe should parse");
|
|
let probe = parse_post_special_conditions_scalar_probe(
|
|
&bytes,
|
|
Some("gmp"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-map-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&special_conditions_probe),
|
|
)
|
|
.expect("post-special-conditions probe should parse");
|
|
|
|
assert_eq!(probe.window_offset, POST_SPECIAL_CONDITIONS_SCALAR_OFFSET);
|
|
assert_eq!(
|
|
probe.window_end_offset,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET
|
|
);
|
|
assert_eq!(probe.dword_count, 79);
|
|
assert_eq!(
|
|
probe.overlap_end_offset,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_OVERLAP_END_OFFSET
|
|
);
|
|
assert_eq!(probe.overlap_dword_count, 14);
|
|
assert_eq!(probe.overlap_nonzero_dword_count, 0);
|
|
assert!(probe.overlap_nonzero_relative_offset_hexes.is_empty());
|
|
assert_eq!(
|
|
probe.tail_offset,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET
|
|
);
|
|
assert_eq!(probe.tail_dword_count, 65);
|
|
assert_eq!(
|
|
probe.tail_runtime_object_offset,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_OFFSET
|
|
);
|
|
assert_eq!(
|
|
probe.tail_runtime_object_end_offset,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_RUNTIME_OBJECT_END_OFFSET
|
|
);
|
|
assert!(!probe.tail_runtime_object_validated_byte_mirror);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_offset,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_OFFSET
|
|
);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_copy_len,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_LEN
|
|
);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_copy_end_offset,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_GROUNDED_TEXT_FIELD_COPY_END_OFFSET
|
|
);
|
|
assert!(probe.tail_window_cuts_through_grounded_live_field);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_remaining_file_window_offset,
|
|
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET
|
|
);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_remaining_file_window_len,
|
|
0x28
|
|
);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_remaining_file_window_nonzero_byte_count,
|
|
0
|
|
);
|
|
assert_eq!(probe.tail_next_grounded_dword_field_offset_hex, "0x4c80");
|
|
assert_eq!(
|
|
probe.tail_next_grounded_dword_field_file_offset_hex,
|
|
"0x0f65"
|
|
);
|
|
assert_eq!(probe.tail_second_grounded_dword_field_offset_hex, "0x4c8c");
|
|
assert_eq!(
|
|
probe.tail_second_grounded_dword_field_file_offset_hex,
|
|
"0x0f71"
|
|
);
|
|
assert!(!probe.post_text_field_file_alignment_matches_grounded_dword_fields);
|
|
assert!(
|
|
probe
|
|
.tail_grounded_live_field_remaining_file_window_first_nonzero_offset
|
|
.is_none()
|
|
);
|
|
assert!(
|
|
probe
|
|
.tail_grounded_live_field_remaining_file_window_last_nonzero_offset
|
|
.is_none()
|
|
);
|
|
assert_eq!(probe.tail_nonzero_dword_count, 0);
|
|
assert!(probe.tail_first_nonzero_offset.is_none());
|
|
assert!(probe.tail_last_nonzero_offset.is_none());
|
|
assert!(probe.tail_nonzero_relative_offset_hexes.is_empty());
|
|
assert_eq!(probe.nonzero_dword_count, 0);
|
|
assert!(probe.first_nonzero_offset.is_none());
|
|
assert!(probe.last_nonzero_offset.is_none());
|
|
assert!(probe.nonzero_lanes.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_zeroed_smp_aligned_runtime_rule_band() {
|
|
let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET];
|
|
let sentinel_offset =
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4;
|
|
bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
|
|
let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None)
|
|
.expect("special-conditions probe should parse");
|
|
let probe = parse_smp_aligned_runtime_rule_band_probe(
|
|
&bytes,
|
|
Some("gmp"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-map-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&special_conditions_probe),
|
|
)
|
|
.expect("aligned runtime-rule band probe should parse");
|
|
|
|
assert_eq!(probe.band_offset, SPECIAL_CONDITIONS_OFFSET);
|
|
assert_eq!(probe.band_end_offset, SMP_ALIGNED_RUNTIME_RULE_END_OFFSET);
|
|
assert_eq!(probe.dword_count, SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT);
|
|
assert_eq!(
|
|
probe.known_editor_rule_dword_count,
|
|
SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT
|
|
);
|
|
assert_eq!(
|
|
probe.post_window_overlap_start_index,
|
|
SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_START_INDEX
|
|
);
|
|
assert_eq!(
|
|
probe.post_window_overlap_dword_count,
|
|
SMP_ALIGNED_RUNTIME_RULE_POST_WINDOW_OVERLAP_DWORD_COUNT
|
|
);
|
|
assert_eq!(probe.nonzero_lane_count, 1);
|
|
assert_eq!(probe.nonzero_band_indices, vec![35]);
|
|
assert!(probe.nonzero_post_window_overlap_band_indices.is_empty());
|
|
assert!(
|
|
probe
|
|
.nonzero_post_window_overlap_post_relative_offset_hexes
|
|
.is_empty()
|
|
);
|
|
assert_eq!(
|
|
probe.nonzero_lanes[0].lane_kind,
|
|
"known-special-condition-dword"
|
|
);
|
|
assert_eq!(
|
|
probe.nonzero_lanes[0].known_label.as_deref(),
|
|
Some("Hidden sentinel")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_nonzero_post_special_conditions_scalar_window() {
|
|
let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_GROUNDED_TEXT_FIELD_FILE_END_OFFSET];
|
|
let sentinel_offset =
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4;
|
|
bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
bytes[0x0df8..0x0dfc].copy_from_slice(&0x413d298cu32.to_le_bytes());
|
|
bytes[0x0e00..0x0e04].copy_from_slice(&0x40e6b756u32.to_le_bytes());
|
|
bytes[0x0f0c..0x0f10].copy_from_slice(&0x42574909u32.to_le_bytes());
|
|
bytes[0x0f34] = 0xaa;
|
|
bytes[0x0f4e] = 0xbb;
|
|
|
|
let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None)
|
|
.expect("special-conditions probe should parse");
|
|
let probe = parse_post_special_conditions_scalar_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&special_conditions_probe),
|
|
)
|
|
.expect("post-special-conditions probe should parse");
|
|
|
|
assert_eq!(probe.nonzero_dword_count, 3);
|
|
assert_eq!(probe.first_nonzero_offset, Some(0x0df8));
|
|
assert_eq!(probe.last_nonzero_offset, Some(0x0f0c));
|
|
assert_eq!(probe.overlap_nonzero_dword_count, 2);
|
|
assert_eq!(
|
|
probe.overlap_nonzero_relative_offset_hexes,
|
|
vec!["0x4".to_string(), "0xc".to_string()]
|
|
);
|
|
assert_eq!(probe.tail_nonzero_dword_count, 1);
|
|
assert_eq!(probe.tail_first_nonzero_offset, Some(0x0f0c));
|
|
assert_eq!(probe.tail_last_nonzero_offset, Some(0x0f0c));
|
|
assert_eq!(
|
|
probe.tail_nonzero_relative_offset_hexes,
|
|
vec!["0x118".to_string()]
|
|
);
|
|
assert_eq!(probe.tail_runtime_object_offset_hex, "0x4b47");
|
|
assert_eq!(probe.tail_runtime_object_end_offset_hex, "0x4c4b");
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_copy_end_offset_hex,
|
|
"0x4c73".to_string()
|
|
);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_name,
|
|
"victory-or-outcome status text buffer"
|
|
);
|
|
assert!(probe.tail_window_cuts_through_grounded_live_field);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_remaining_file_window_len_hex,
|
|
"0x28"
|
|
);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_remaining_file_window_nonzero_byte_count,
|
|
2
|
|
);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_remaining_file_window_first_nonzero_offset,
|
|
Some(0x0f34)
|
|
);
|
|
assert_eq!(
|
|
probe.tail_grounded_live_field_remaining_file_window_last_nonzero_offset,
|
|
Some(0x0f4e)
|
|
);
|
|
assert_eq!(probe.tail_next_grounded_dword_field_file_offset, 0x0f65);
|
|
assert_eq!(probe.tail_second_grounded_dword_field_file_offset, 0x0f71);
|
|
assert!(!probe.post_text_field_file_alignment_matches_grounded_dword_fields);
|
|
assert_eq!(probe.nonzero_lanes[0].relative_offset, 0x04);
|
|
assert_eq!(probe.nonzero_lanes[1].relative_offset, 0x0c);
|
|
assert_eq!(probe.nonzero_lanes[2].relative_offset, 0x118);
|
|
assert!(
|
|
probe
|
|
.nonzero_lanes
|
|
.iter()
|
|
.all(|lane| lane.probable_f32_le.is_some())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_post_text_field_neighborhood_probe() {
|
|
let mut bytes = vec![0u8; POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET];
|
|
let sentinel_offset =
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4;
|
|
bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
bytes[0x0f59] = 0x01;
|
|
bytes[0x0f5d] = 0x02;
|
|
bytes[0x0f61] = 0x03;
|
|
bytes[0x0f6d] = 0x04;
|
|
bytes[0x0f5c..0x0f60].copy_from_slice(&0x40f33333u32.to_le_bytes());
|
|
bytes[0x0f6c..0x0f70].copy_from_slice(&0x40c08cfbu32.to_le_bytes());
|
|
|
|
let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None)
|
|
.expect("special-conditions probe should parse");
|
|
let probe = parse_post_text_field_neighborhood_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&special_conditions_probe),
|
|
)
|
|
.expect("post-text field neighborhood probe should parse");
|
|
|
|
assert_eq!(probe.window_offset, POST_TEXT_FIELD_NEIGHBORHOOD_OFFSET);
|
|
assert_eq!(
|
|
probe.window_end_offset,
|
|
POST_TEXT_FIELD_NEIGHBORHOOD_END_OFFSET
|
|
);
|
|
assert_eq!(probe.grounded_field_observations.len(), 6);
|
|
assert_eq!(
|
|
probe.grounded_field_observations[0].field_name,
|
|
"Auto-Show Grade During Track Lay"
|
|
);
|
|
assert_eq!(probe.grounded_field_observations[0].value_u8, Some(0x01));
|
|
assert_eq!(
|
|
probe.grounded_field_observations[3].field_name,
|
|
"leftover simulation time accumulator"
|
|
);
|
|
assert_eq!(probe.one_byte_early_float_candidates.len(), 2);
|
|
assert_eq!(
|
|
probe.one_byte_early_float_candidates[0].grounded_field_name,
|
|
"Starting Building Density Level"
|
|
);
|
|
assert_eq!(
|
|
probe.one_byte_early_float_candidates[0].candidate_offset_hex,
|
|
"0x0f5c"
|
|
);
|
|
assert_eq!(
|
|
probe.one_byte_early_float_candidates[1].grounded_field_name,
|
|
"selected-year lane snapshot"
|
|
);
|
|
assert_eq!(
|
|
probe.one_byte_early_float_candidates[1].candidate_offset_hex,
|
|
"0x0f6c"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_locomotive_policy_neighborhood_probe() {
|
|
let mut bytes = vec![0u8; LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET];
|
|
let sentinel_offset =
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4;
|
|
bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
bytes[0x0f78] = 0x01;
|
|
bytes[0x0f7c] = 0x02;
|
|
bytes[0x0f7d] = 0x03;
|
|
bytes[0x0f7e] = 0x04;
|
|
bytes[0x0f9c..0x0fa0].copy_from_slice(&0x42c1c036u32.to_le_bytes());
|
|
bytes[0x0fa0..0x0fa4].copy_from_slice(&0x433a7abeu32.to_le_bytes());
|
|
|
|
let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None)
|
|
.expect("special-conditions probe should parse");
|
|
let probe = parse_locomotive_policy_neighborhood_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&special_conditions_probe),
|
|
)
|
|
.expect("locomotive policy neighborhood probe should parse");
|
|
|
|
assert_eq!(probe.window_offset, LOCOMOTIVE_POLICY_NEIGHBORHOOD_OFFSET);
|
|
assert_eq!(
|
|
probe.window_end_offset,
|
|
LOCOMOTIVE_POLICY_NEIGHBORHOOD_END_OFFSET
|
|
);
|
|
assert_eq!(probe.grounded_field_observations.len(), 9);
|
|
assert_eq!(
|
|
probe.grounded_field_observations[0].field_name,
|
|
"selected-year bucket companion scalar"
|
|
);
|
|
assert_eq!(
|
|
probe.grounded_field_observations[4].field_name,
|
|
"All Steam Locos Avail."
|
|
);
|
|
assert_eq!(probe.grounded_field_observations[4].value_u8, Some(0x02));
|
|
assert_eq!(
|
|
probe.grounded_field_observations[8].field_name,
|
|
"cached available-locomotive rating"
|
|
);
|
|
assert_eq!(probe.three_byte_early_float_candidates.len(), 2);
|
|
assert_eq!(
|
|
probe.three_byte_early_float_candidates[0].grounded_field_name,
|
|
"station-list selected station id"
|
|
);
|
|
assert_eq!(
|
|
probe.three_byte_early_float_candidates[0].candidate_offset_hex,
|
|
"0x0f9c"
|
|
);
|
|
assert_eq!(
|
|
probe.three_byte_early_float_candidates[1].grounded_field_name,
|
|
"cached available-locomotive rating"
|
|
);
|
|
assert_eq!(
|
|
probe.three_byte_early_float_candidates[1].candidate_offset_hex,
|
|
"0x0fa0"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_pre_recipe_scalar_plateau_probe() {
|
|
let mut bytes = vec![0u8; PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET];
|
|
let sentinel_offset =
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4;
|
|
bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
bytes[0x0fa7..0x0fab].copy_from_slice(&0x82839300u32.to_le_bytes());
|
|
bytes[0x0fab..0x0faf].copy_from_slice(&0x948c9949u32.to_le_bytes());
|
|
bytes[0x0faf..0x0fb3].copy_from_slice(&0x8000003fu32.to_le_bytes());
|
|
bytes[0x0fb3..0x0fb7].copy_from_slice(&0x75c28f3fu32.to_le_bytes());
|
|
bytes[0x0fcb..0x0fcf].copy_from_slice(&0x00300000u32.to_le_bytes());
|
|
bytes[0x0fdb..0x0fdf].copy_from_slice(&0x00ffea22u32.to_le_bytes());
|
|
|
|
let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None)
|
|
.expect("special-conditions probe should parse");
|
|
let probe = parse_pre_recipe_scalar_plateau_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&special_conditions_probe),
|
|
)
|
|
.expect("pre-recipe scalar plateau probe should parse");
|
|
|
|
assert_eq!(probe.window_offset, PRE_RECIPE_SCALAR_PLATEAU_OFFSET);
|
|
assert_eq!(
|
|
probe.window_end_offset,
|
|
PRE_RECIPE_SCALAR_PLATEAU_END_OFFSET
|
|
);
|
|
assert_eq!(probe.family_signature, "rt3-105-base-pre-recipe-plateau-v1");
|
|
assert_eq!(probe.nonzero_lanes[0].absolute_offset_hex, "0x0fa7");
|
|
assert_eq!(probe.nonzero_lanes[2].absolute_offset_hex, "0x0faf");
|
|
assert_eq!(probe.nonzero_lanes[2].value_hex, "0x8000003f");
|
|
}
|
|
|
|
#[test]
|
|
fn parses_recipe_book_summary_probe() {
|
|
let mut bytes = vec![0u8; RECIPE_BOOK_SUMMARY_END_OFFSET];
|
|
let sentinel_offset =
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4;
|
|
bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
|
|
let book0 = RECIPE_BOOK_ROOT_OFFSET;
|
|
bytes[book0..book0 + 16].copy_from_slice(&[
|
|
0x11, 0x22, 0x33, 0x44, 0xaa, 0xbb, 0xcc, 0xdd, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60,
|
|
0x70, 0x80,
|
|
]);
|
|
bytes[book0 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET
|
|
..book0 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + 4]
|
|
.copy_from_slice(&0x41200000u32.to_le_bytes());
|
|
bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET
|
|
..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + RECIPE_BOOK_LINE_AREA_LEN]
|
|
.fill(0xcd);
|
|
bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 4]
|
|
.copy_from_slice(&0x00000003u32.to_le_bytes());
|
|
bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 4..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 8]
|
|
.copy_from_slice(&0x41a00000u32.to_le_bytes());
|
|
bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 8..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 12]
|
|
.copy_from_slice(&0x00000017u32.to_le_bytes());
|
|
bytes[book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 0x1c
|
|
..book0 + RECIPE_BOOK_LINE_AREA_OFFSET + 0x20]
|
|
.copy_from_slice(&0x0000002au32.to_le_bytes());
|
|
|
|
let book1 = RECIPE_BOOK_ROOT_OFFSET + RECIPE_BOOK_STRIDE;
|
|
bytes[book1 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET
|
|
..book1 + RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET + 4]
|
|
.copy_from_slice(&0x00000000u32.to_le_bytes());
|
|
|
|
let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gmp"), None)
|
|
.expect("special-conditions probe should parse");
|
|
let probe = parse_recipe_book_summary_probe(
|
|
&bytes,
|
|
Some("gmp"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-map-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&special_conditions_probe),
|
|
)
|
|
.expect("recipe-book summary probe should parse");
|
|
|
|
assert_eq!(probe.root_offset, RECIPE_BOOK_ROOT_OFFSET);
|
|
assert_eq!(probe.book_count, RECIPE_BOOK_COUNT);
|
|
assert_eq!(probe.book_stride, RECIPE_BOOK_STRIDE);
|
|
assert_eq!(
|
|
probe.max_annual_production_relative_offset,
|
|
RECIPE_BOOK_MAX_ANNUAL_PRODUCTION_OFFSET
|
|
);
|
|
assert_eq!(probe.books[0].head_kind, "mixed");
|
|
assert_eq!(probe.books[0].line_area_kind, "mixed");
|
|
assert_eq!(probe.books[0].max_annual_production_word_hex, "0x41200000");
|
|
assert_eq!(
|
|
probe.books[0]
|
|
.max_annual_production_probable_f32_le
|
|
.as_deref(),
|
|
Some("10.000000")
|
|
);
|
|
assert_eq!(probe.books[0].lines.len(), RECIPE_BOOK_LINE_COUNT);
|
|
assert_eq!(probe.books[0].lines[0].line_kind, "mixed");
|
|
assert_eq!(probe.books[0].lines[0].mode_word_hex, "0x00000003");
|
|
assert_eq!(probe.books[0].lines[0].annual_amount_word_hex, "0x41a00000");
|
|
assert_eq!(
|
|
probe.books[0].lines[0]
|
|
.annual_amount_probable_f32_le
|
|
.as_deref(),
|
|
Some("20.000000")
|
|
);
|
|
assert_eq!(
|
|
probe.books[0].lines[0].supplied_cargo_token_word_hex,
|
|
"0x00000017"
|
|
);
|
|
assert_eq!(
|
|
probe.books[0].lines[0].supplied_cargo_token_window_hex,
|
|
"17000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd2a000000"
|
|
);
|
|
assert_eq!(
|
|
probe.books[0].lines[0].supplied_cargo_token_window_ascii,
|
|
"....................*..."
|
|
);
|
|
assert!(probe.books[0].lines[0].supplied_cargo_token_active_in_runtime_import);
|
|
assert_eq!(
|
|
probe.books[0].lines[0].demanded_cargo_token_word_hex,
|
|
"0x0000002a"
|
|
);
|
|
assert_eq!(
|
|
probe.books[0].lines[0].demanded_cargo_token_window_hex,
|
|
"2a000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd"
|
|
);
|
|
assert_eq!(
|
|
probe.books[0].lines[0].demanded_cargo_token_window_ascii,
|
|
"*..................."
|
|
);
|
|
assert!(probe.books[0].lines[0].demanded_cargo_token_active_in_runtime_import);
|
|
assert_eq!(probe.books[1].head_kind, "zero");
|
|
assert_eq!(probe.books[1].line_area_kind, "zero");
|
|
assert_eq!(probe.books[1].lines[0].line_kind, "zero");
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_probable_recipe_token_high16_ascii_stem() {
|
|
assert_eq!(
|
|
probable_recipe_token_high16_ascii_stem(0x72470000).as_deref(),
|
|
Some("Gr")
|
|
);
|
|
assert_eq!(
|
|
probable_recipe_token_high16_ascii_stem(0x68430000).as_deref(),
|
|
Some("Ch")
|
|
);
|
|
assert_eq!(probable_recipe_token_high16_ascii_stem(0x000040a0), None);
|
|
assert_eq!(probable_recipe_token_high16_ascii_stem(0x00170000), None);
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_recipe_token_layouts() {
|
|
assert_eq!(classify_recipe_token_layout(0x00000000), "zero");
|
|
assert_eq!(
|
|
classify_recipe_token_layout(0x72470000),
|
|
"high16-ascii-stem"
|
|
);
|
|
assert_eq!(classify_recipe_token_layout(0x00170000), "high16-numeric");
|
|
assert_eq!(classify_recipe_token_layout(0x000040a0), "low16-marker");
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_recipe_line_signatures() {
|
|
assert_eq!(
|
|
classify_recipe_line_signature(0x00000000, 0x00000000, 0x00010000),
|
|
"demand-numeric-entry"
|
|
);
|
|
assert_eq!(
|
|
classify_recipe_line_signature(0x00000000, 0x00000000, 0x72470000),
|
|
"demand-stem-entry"
|
|
);
|
|
assert_eq!(
|
|
classify_recipe_line_signature(0x00000000, 0x00170000, 0x00000000),
|
|
"supply-numeric-entry"
|
|
);
|
|
assert_eq!(
|
|
classify_recipe_line_signature(0x00110000, 0x000040a0, 0x00000000),
|
|
"supply-marker-entry"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_recipe_runtime_import_branches() {
|
|
assert_eq!(
|
|
classify_recipe_runtime_import_branch(0),
|
|
"zero-mode-skipped"
|
|
);
|
|
assert_eq!(
|
|
classify_recipe_runtime_import_branch(1),
|
|
"mode1-demand-branch"
|
|
);
|
|
assert_eq!(
|
|
classify_recipe_runtime_import_branch(3),
|
|
"mode3-dual-branch"
|
|
);
|
|
assert_eq!(
|
|
classify_recipe_runtime_import_branch(0x00110000),
|
|
"nonzero-supply-branch"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_nonzero_smp_aligned_runtime_rule_band() {
|
|
let mut bytes = vec![0u8; POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET];
|
|
let sentinel_offset =
|
|
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4;
|
|
bytes[sentinel_offset..sentinel_offset + 4].copy_from_slice(&1u32.to_le_bytes());
|
|
bytes[0x0df8..0x0dfc].copy_from_slice(&0x413d298cu32.to_le_bytes());
|
|
bytes[0x0e00..0x0e04].copy_from_slice(&0x40e6b756u32.to_le_bytes());
|
|
bytes[0x0e18..0x0e1c].copy_from_slice(&0x41d4ccceu32.to_le_bytes());
|
|
bytes[0x0e24..0x0e28].copy_from_slice(&0x3fd2b549u32.to_le_bytes());
|
|
|
|
let special_conditions_probe = parse_special_conditions_probe(&bytes, Some("gms"), None)
|
|
.expect("special-conditions probe should parse");
|
|
let probe = parse_smp_aligned_runtime_rule_band_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&special_conditions_probe),
|
|
)
|
|
.expect("aligned runtime-rule band probe should parse");
|
|
|
|
assert_eq!(probe.nonzero_band_indices, vec![35, 37, 39, 45, 48]);
|
|
assert_eq!(
|
|
probe.nonzero_post_window_overlap_band_indices,
|
|
vec![37, 39, 45, 48]
|
|
);
|
|
assert_eq!(
|
|
probe.nonzero_post_window_overlap_post_relative_offset_hexes,
|
|
vec![
|
|
"0x4".to_string(),
|
|
"0xc".to_string(),
|
|
"0x24".to_string(),
|
|
"0x30".to_string()
|
|
]
|
|
);
|
|
assert_eq!(probe.nonzero_lanes[1].relative_offset, 0x94);
|
|
assert_eq!(
|
|
probe.nonzero_lanes[1].lane_kind,
|
|
"unlabeled-editor-rule-dword"
|
|
);
|
|
assert!(probe.nonzero_lanes[1].probable_f32_le.is_some());
|
|
assert_eq!(probe.nonzero_lanes.last().unwrap().band_index, 48);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_save_anchor_cycle_and_trailer() {
|
|
let cycle_words: [u32; 9] = [
|
|
0x00000000, 0x0186a000, 0x00000000, 0x86a00000, 0x00000001, 0xa0000000, 0x00000186,
|
|
0x00000000, 0x000186a0,
|
|
];
|
|
let trailer_words: [u32; 3] = [0x00020000, 0x00030000, 0x2ee10000];
|
|
let mut bytes = vec![0u8; 0x1c + (cycle_words.len() * 2 + 2 + trailer_words.len()) * 4];
|
|
|
|
let mut cursor = 0x1c;
|
|
for _ in 0..2 {
|
|
for word in cycle_words {
|
|
bytes[cursor..cursor + 4].copy_from_slice(&word.to_le_bytes());
|
|
cursor += 4;
|
|
}
|
|
}
|
|
for word in &cycle_words[..2] {
|
|
bytes[cursor..cursor + 4].copy_from_slice(&(*word).to_le_bytes());
|
|
cursor += 4;
|
|
}
|
|
for word in trailer_words {
|
|
bytes[cursor..cursor + 4].copy_from_slice(&word.to_le_bytes());
|
|
cursor += 4;
|
|
}
|
|
|
|
let container_profile = SmpContainerProfile {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
profile_evidence: vec!["test".to_string()],
|
|
is_known_profile: true,
|
|
};
|
|
let bootstrap = SmpSaveBootstrapBlock {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
aligned_window_offset: 0,
|
|
leading_word: 0,
|
|
leading_word_hex: "0x00000000".to_string(),
|
|
anchor_word: 0,
|
|
anchor_word_hex: "0x00000000".to_string(),
|
|
descriptor_word_2: 0,
|
|
descriptor_word_2_hex: "0x00000000".to_string(),
|
|
descriptor_word_3: 0,
|
|
descriptor_word_3_hex: "0x00000000".to_string(),
|
|
descriptor_word_4: 0,
|
|
descriptor_word_4_hex: "0x00000000".to_string(),
|
|
descriptor_word_5: 0,
|
|
descriptor_word_5_hex: "0x00000000".to_string(),
|
|
descriptor_word_6: 0,
|
|
descriptor_word_6_hex: "0x00000000".to_string(),
|
|
descriptor_word_7: 0,
|
|
descriptor_word_7_hex: "0x00000000".to_string(),
|
|
};
|
|
|
|
let parsed =
|
|
parse_save_anchor_run_block(&bytes, Some(&container_profile), Some(&bootstrap))
|
|
.expect("cycle block should parse");
|
|
|
|
assert_eq!(parsed.cycle_start_offset, 0x1c);
|
|
assert_eq!(parsed.cycle_words, cycle_words);
|
|
assert_eq!(parsed.full_cycle_count, 2);
|
|
assert_eq!(parsed.partial_cycle_word_count, 2);
|
|
assert_eq!(
|
|
parsed.trailer_offset,
|
|
0x1c + (cycle_words.len() * 2 + 2) * 4
|
|
);
|
|
assert_eq!(parsed.trailer_words, trailer_words);
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_runtime_trailer_family() {
|
|
let runtime_anchor_cycle_block = SmpRuntimeAnchorCycleBlock {
|
|
profile_family: "rt3-classic-sandbox-container-v1".to_string(),
|
|
cycle_start_offset: 0x33c,
|
|
cycle_words: vec![0; 9],
|
|
cycle_hex_words: vec!["0x00000000".to_string(); 9],
|
|
full_cycle_count: 3,
|
|
partial_cycle_word_count: 2,
|
|
trailer_offset: 0x3b0,
|
|
trailer_words: vec![
|
|
0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, 0x2ee10000,
|
|
0x32c80000, 0x0dcd0000, 0x01010107, 0x26010000, 0x01010107, 0x00010000, 0x0334c68c,
|
|
0x03000000, 0x01000000,
|
|
],
|
|
trailer_hex_words: Vec::new(),
|
|
};
|
|
let container_profile = SmpContainerProfile {
|
|
profile_family: "rt3-classic-sandbox-container-v1".to_string(),
|
|
profile_evidence: vec!["test".to_string()],
|
|
is_known_profile: true,
|
|
};
|
|
|
|
let trailer = parse_runtime_trailer_block(
|
|
Some(&container_profile),
|
|
Some(&runtime_anchor_cycle_block),
|
|
)
|
|
.expect("runtime trailer should parse");
|
|
|
|
assert_eq!(trailer.trailer_family, "rt3-classic-sandbox-trailer-v1");
|
|
assert_eq!(trailer.prefix_words_0_to_5[0], 0x00010000);
|
|
assert_eq!(trailer.tag_word_6, 0x2ee10000);
|
|
assert_eq!(trailer.tag_chunk_id_u16, 0x2ee1);
|
|
assert_eq!(trailer.selector_word_8, 0x0dcd0000);
|
|
assert_eq!(trailer.selector_high_u16, 0x0dcd);
|
|
assert_eq!(trailer.mode_word_15, 0x01000000);
|
|
}
|
|
|
|
#[test]
|
|
fn probes_runtime_post_span_region() {
|
|
let mut bytes = vec![0u8; 0x200];
|
|
bytes[0x90..0x94].copy_from_slice(&0x32dc0000u32.to_le_bytes());
|
|
bytes[0x94..0x98].copy_from_slice(&0x37140000u32.to_le_bytes());
|
|
bytes[0x98..0x9c].copy_from_slice(&0x03000000u32.to_le_bytes());
|
|
bytes[0xa0..0xa4].copy_from_slice(&0x37150000u32.to_le_bytes());
|
|
bytes[0xa4..0xa8].copy_from_slice(&0x00010000u32.to_le_bytes());
|
|
bytes[0xa8..0xac].copy_from_slice(&0x00410000u32.to_le_bytes());
|
|
|
|
let trailer = SmpRuntimeTrailerBlock {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
trailer_family: "test".to_string(),
|
|
trailer_evidence: Vec::new(),
|
|
trailer_offset: 0x40,
|
|
prefix_words_0_to_5: Vec::new(),
|
|
prefix_hex_words_0_to_5: Vec::new(),
|
|
tag_word_6: 0x2ee10000,
|
|
tag_word_6_hex: "0x2ee10000".to_string(),
|
|
tag_chunk_id_u16: 0x2ee1,
|
|
tag_chunk_id_hex: "0x2ee1".to_string(),
|
|
tag_chunk_id_grounded_alignment: None,
|
|
length_word_7: 0x00200000,
|
|
length_word_7_hex: "0x00200000".to_string(),
|
|
length_high_u16: 0x0020,
|
|
length_high_hex: "0x0020".to_string(),
|
|
selector_word_8: 0,
|
|
selector_word_8_hex: "0x00000000".to_string(),
|
|
selector_high_u16: 0,
|
|
selector_high_hex: "0x0000".to_string(),
|
|
layout_word_9: 0,
|
|
layout_word_9_hex: "0x00000000".to_string(),
|
|
descriptor_word_10: 0,
|
|
descriptor_word_10_hex: "0x00000000".to_string(),
|
|
descriptor_high_u16: 0,
|
|
descriptor_high_hex: "0x0000".to_string(),
|
|
descriptor_word_11: 0,
|
|
descriptor_word_11_hex: "0x00000000".to_string(),
|
|
counter_word_12: 0,
|
|
counter_word_12_hex: "0x00000000".to_string(),
|
|
offset_word_13: 0,
|
|
offset_word_13_hex: "0x00000000".to_string(),
|
|
span_word_14: 0,
|
|
span_word_14_hex: "0x00000000".to_string(),
|
|
mode_word_15: 0,
|
|
mode_word_15_hex: "0x00000000".to_string(),
|
|
words: Vec::new(),
|
|
hex_words: Vec::new(),
|
|
};
|
|
|
|
let probe = parse_runtime_post_span_probe(&bytes, Some(&trailer))
|
|
.expect("post-span probe should parse");
|
|
|
|
assert_eq!(probe.span_target_offset, 0x60);
|
|
assert_eq!(probe.next_nonzero_offset, Some(0x92));
|
|
assert_eq!(probe.next_aligned_candidate_offset, Some(0x8c));
|
|
assert_eq!(probe.header_candidates.len(), 1);
|
|
assert_eq!(probe.header_candidates[0].dense_word_count, 3);
|
|
assert_eq!(probe.header_candidates[0].grounded_alignments.len(), 2);
|
|
assert_eq!(probe.grounded_progress_hits[0], "0x32dc@0x00000090");
|
|
}
|
|
|
|
#[test]
|
|
fn parses_classic_rehydrate_profile_probe() {
|
|
let mut bytes = vec![0u8; 0x220];
|
|
bytes[0x90..0x94].copy_from_slice(&0x32dc0000u32.to_le_bytes());
|
|
bytes[0x94..0x98].copy_from_slice(&0x37140000u32.to_le_bytes());
|
|
bytes[0x1a0..0x1a4].copy_from_slice(&0x37150000u32.to_le_bytes());
|
|
bytes[0xab..0xb7].copy_from_slice(b"test-map.gmp");
|
|
bytes[0xde..0xe6].copy_from_slice(b"Test Map");
|
|
|
|
let post_span = SmpRuntimePostSpanProbe {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
span_target_offset: 0,
|
|
next_nonzero_offset: Some(0x92),
|
|
next_aligned_candidate_offset: Some(0x8c),
|
|
next_aligned_candidate_words: vec![0, 0x32dc0000, 0x37140000, 0x03000000],
|
|
next_aligned_candidate_hex_words: vec![],
|
|
header_candidates: vec![],
|
|
grounded_progress_hits: vec![
|
|
"0x32dc@0x00000090".to_string(),
|
|
"0x3714@0x00000094".to_string(),
|
|
"0x3715@0x000001a0".to_string(),
|
|
],
|
|
};
|
|
|
|
let probe = parse_classic_rehydrate_profile_probe(&bytes, Some(&post_span))
|
|
.expect("classic rehydrate probe should parse");
|
|
|
|
assert_eq!(probe.packed_profile_offset, 0x98);
|
|
assert_eq!(probe.packed_profile_len, 0x108);
|
|
assert_eq!(probe.ascii_runs[0].preview, "test-map.gmp");
|
|
assert_eq!(probe.packed_profile_block.leading_word_0, 0x00000000);
|
|
assert_eq!(
|
|
probe.packed_profile_block.map_path.as_deref(),
|
|
Some("test-map.gmp")
|
|
);
|
|
assert_eq!(
|
|
probe.packed_profile_block.display_name.as_deref(),
|
|
Some("Test Map")
|
|
);
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x77, 0x00);
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x82, 0x00);
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x97, 0x00);
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0xc5, 0x00);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_rt3_105_packed_profile_probe() {
|
|
let mut bytes = vec![0u8; 0x9000];
|
|
let block = 0x73c0usize;
|
|
bytes[block..block + 4].copy_from_slice(&0x00000003u32.to_le_bytes());
|
|
bytes[block + 0x0c..block + 0x10].copy_from_slice(&0x01000000u32.to_le_bytes());
|
|
bytes[block + 0x10..block + 0x1d].copy_from_slice(b"test-105.gmp\0");
|
|
bytes[block + 0x43..block + 0x4c].copy_from_slice(b"Test 105\0");
|
|
bytes[block + 0x77] = 0x07;
|
|
bytes[block + 0x82] = 0x4d;
|
|
bytes[block + 0x84..block + 0x88].copy_from_slice(&0x65010000u32.to_le_bytes());
|
|
|
|
let header_variant_probe = SmpHeaderVariantProbe {
|
|
variant_family: "rt3-105-common-header-v1".to_string(),
|
|
variant_evidence: vec![],
|
|
is_known_family: true,
|
|
};
|
|
let probe = parse_rt3_105_packed_profile_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&header_variant_probe),
|
|
None,
|
|
)
|
|
.expect("1.05 packed profile probe should parse");
|
|
|
|
assert_eq!(probe.profile_family, "rt3-105-save-analog-block-inferred");
|
|
assert_eq!(probe.packed_profile_offset, 0x73c0);
|
|
assert_eq!(probe.packed_profile_len, 0x108);
|
|
assert_eq!(
|
|
probe.packed_profile_block.map_path.as_deref(),
|
|
Some("test-105.gmp")
|
|
);
|
|
assert_eq!(
|
|
probe.packed_profile_block.display_name.as_deref(),
|
|
Some("Test 105")
|
|
);
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x77, 0x07);
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x82, 0x4d);
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x97, 0x00);
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0xc5, 0x00);
|
|
}
|
|
|
|
#[test]
|
|
fn builds_classic_save_load_summary() {
|
|
let summary = build_save_load_summary(
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
None,
|
|
None,
|
|
Some(&SmpClassicRehydrateProfileProbe {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
progress_32dc_offset: 0x76e8,
|
|
progress_3714_offset: 0x76ec,
|
|
progress_3715_offset: 0x77f8,
|
|
packed_profile_offset: 0x76f0,
|
|
packed_profile_len: 0x108,
|
|
packed_profile_len_hex: "0x108".to_string(),
|
|
packed_profile_block: SmpClassicPackedProfileBlock {
|
|
relative_len: 0x108,
|
|
relative_len_hex: "0x108".to_string(),
|
|
leading_word_0: 3,
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
|
trailing_zero_word_count_after_leading_word: 3,
|
|
map_path_offset: 0x13,
|
|
map_path: Some("British Isles.gmp".to_string()),
|
|
display_name_offset: 0x46,
|
|
display_name: Some("British Isles".to_string()),
|
|
profile_byte_0x77: 0,
|
|
profile_byte_0x77_hex: "0x00".to_string(),
|
|
profile_byte_0x82: 0,
|
|
profile_byte_0x82_hex: "0x00".to_string(),
|
|
profile_byte_0x97: 0,
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
|
profile_byte_0xc5: 0,
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
|
stable_nonzero_words: vec![],
|
|
},
|
|
ascii_runs: vec![],
|
|
}),
|
|
None,
|
|
None,
|
|
)
|
|
.expect("classic summary");
|
|
|
|
assert_eq!(summary.mechanism_family, "classic-save-rehydrate-v1");
|
|
assert_eq!(summary.mechanism_confidence, "grounded");
|
|
assert_eq!(summary.map_path.as_deref(), Some("British Isles.gmp"));
|
|
assert_eq!(summary.packed_profile_len, Some(0x108));
|
|
}
|
|
|
|
#[test]
|
|
fn builds_rt3_105_save_load_summary_with_candidate_table() {
|
|
let summary = build_save_load_summary(
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
None,
|
|
Some(&SmpRt3105PostSpanBridgeProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
|
bridge_evidence: vec![],
|
|
span_target_offset: 0x3678,
|
|
next_candidate_offset: Some(0x4f14),
|
|
next_candidate_delta_from_span_target: Some(0x189c),
|
|
packed_profile_offset: 0x73c0,
|
|
packed_profile_delta_from_span_target: 0x3d48,
|
|
next_candidate_delta_from_packed_profile: Some(-0x24ac),
|
|
selector_high_u16: 0x7110,
|
|
selector_high_hex: "0x7110".to_string(),
|
|
descriptor_high_u16: 0x7801,
|
|
descriptor_high_hex: "0x7801".to_string(),
|
|
next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515],
|
|
next_candidate_high_hex_words: vec![],
|
|
}),
|
|
None,
|
|
Some(&SmpRt3105PackedProfileProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
packed_profile_offset: 0x73c0,
|
|
packed_profile_len: 0x108,
|
|
packed_profile_len_hex: "0x108".to_string(),
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
|
relative_len: 0x108,
|
|
relative_len_hex: "0x108".to_string(),
|
|
leading_word_0: 3,
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
|
trailing_zero_word_count_after_leading_word: 2,
|
|
header_flag_word_3: 1,
|
|
header_flag_word_3_hex: "0x00000001".to_string(),
|
|
map_path_offset: 0x10,
|
|
map_path: Some("Alternate USA.gmp".to_string()),
|
|
display_name_offset: 0x43,
|
|
display_name: Some("Alternate USA".to_string()),
|
|
profile_byte_0x77: 0x07,
|
|
profile_byte_0x77_hex: "0x07".to_string(),
|
|
profile_byte_0x82: 0x4d,
|
|
profile_byte_0x82_hex: "0x4d".to_string(),
|
|
profile_byte_0x97: 0,
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
|
profile_byte_0xc5: 0,
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
|
stable_nonzero_words: vec![],
|
|
},
|
|
ascii_runs: vec![],
|
|
}),
|
|
Some(&SmpRt3105SaveNameTableProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-bridge-secondary-block".to_string(),
|
|
semantic_family: "scenario-named-candidate-availability-table".to_string(),
|
|
semantic_alignment: vec![],
|
|
header_offset: 0x6a70,
|
|
header_word_0: 0,
|
|
header_word_0_hex: "0x00000000".to_string(),
|
|
header_word_1: 0,
|
|
header_word_1_hex: "0x00000000".to_string(),
|
|
header_word_2: 0x332e,
|
|
header_word_2_hex: "0x0000332e".to_string(),
|
|
entry_stride: 0x22,
|
|
entry_stride_hex: "0x22".to_string(),
|
|
header_prefix_word_count: 11,
|
|
observed_entry_capacity: 0x44,
|
|
observed_entry_count: 67,
|
|
zero_trailer_entry_count: 3,
|
|
nonzero_trailer_entry_count: 64,
|
|
distinct_trailer_words: vec![0, 1],
|
|
distinct_trailer_hex_words: vec![
|
|
"0x00000000".to_string(),
|
|
"0x00000001".to_string(),
|
|
],
|
|
zero_trailer_entry_names: vec![
|
|
"Nuclear Power Plant".to_string(),
|
|
"Recycling Plant".to_string(),
|
|
"Uranium Mine".to_string(),
|
|
],
|
|
entries_offset: 0x6ad1,
|
|
entries_end_offset: 0x73b7,
|
|
trailing_footer_hex: "dc3200001437000000".to_string(),
|
|
footer_progress_word_0: 0x32dc,
|
|
footer_progress_word_0_hex: "0x000032dc".to_string(),
|
|
footer_progress_word_1: 0x3714,
|
|
footer_progress_word_1_hex: "0x00003714".to_string(),
|
|
footer_trailing_byte: 0,
|
|
footer_trailing_byte_hex: "0x00".to_string(),
|
|
footer_grounded_alignments: vec![],
|
|
entries: vec![],
|
|
evidence: vec![],
|
|
}),
|
|
)
|
|
.expect("1.05 summary");
|
|
|
|
assert_eq!(summary.mechanism_family, "rt3-105-save-post-span-bridge-v1");
|
|
assert_eq!(summary.mechanism_confidence, "mixed");
|
|
assert_eq!(summary.map_path.as_deref(), Some("Alternate USA.gmp"));
|
|
assert_eq!(
|
|
summary
|
|
.candidate_table
|
|
.as_ref()
|
|
.expect("candidate table")
|
|
.zero_availability_count,
|
|
3
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn loads_classic_save_slice_from_report() {
|
|
let mut report = inspect_smp_bytes(&[]);
|
|
let classic_probe = SmpClassicRehydrateProfileProbe {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
progress_32dc_offset: 0x76e8,
|
|
progress_3714_offset: 0x76ec,
|
|
progress_3715_offset: 0x77f8,
|
|
packed_profile_offset: 0x76f0,
|
|
packed_profile_len: 0x108,
|
|
packed_profile_len_hex: "0x108".to_string(),
|
|
packed_profile_block: SmpClassicPackedProfileBlock {
|
|
relative_len: 0x108,
|
|
relative_len_hex: "0x108".to_string(),
|
|
leading_word_0: 3,
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
|
trailing_zero_word_count_after_leading_word: 3,
|
|
map_path_offset: 0x13,
|
|
map_path: Some("British Isles.gmp".to_string()),
|
|
display_name_offset: 0x46,
|
|
display_name: Some("British Isles".to_string()),
|
|
profile_byte_0x77: 0,
|
|
profile_byte_0x77_hex: "0x00".to_string(),
|
|
profile_byte_0x82: 0,
|
|
profile_byte_0x82_hex: "0x00".to_string(),
|
|
profile_byte_0x97: 0,
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
|
profile_byte_0xc5: 0,
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
|
stable_nonzero_words: vec![],
|
|
},
|
|
ascii_runs: vec![],
|
|
};
|
|
report.classic_rehydrate_profile_probe = Some(classic_probe.clone());
|
|
report.save_load_summary = build_save_load_summary(
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
None,
|
|
None,
|
|
Some(&classic_probe),
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let slice = load_save_slice_from_report(&report).expect("classic save slice");
|
|
assert_eq!(slice.mechanism_family, "classic-save-rehydrate-v1");
|
|
assert_eq!(
|
|
slice
|
|
.profile
|
|
.as_ref()
|
|
.and_then(|profile| profile.map_path.as_deref()),
|
|
Some("British Isles.gmp")
|
|
);
|
|
assert!(slice.candidate_availability_table.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_event_runtime_collection_summary_from_synthetic_chunks() {
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.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.to_le_bytes());
|
|
bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
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, 96);
|
|
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<u8> {
|
|
let mut bytes = Vec::with_capacity(1 + text.len());
|
|
bytes.push(text.len() as u8);
|
|
bytes.extend_from_slice(text.as_bytes());
|
|
bytes
|
|
}
|
|
|
|
fn encode_template(
|
|
record_id: u32,
|
|
trigger_kind: u8,
|
|
flags: u8,
|
|
actions: &[Vec<u8>],
|
|
) -> Vec<u8> {
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(PACKED_EVENT_RECORD_TEMPLATE_SYNTHETIC_MAGIC);
|
|
bytes.extend_from_slice(&record_id.to_le_bytes());
|
|
bytes.push(trigger_kind);
|
|
bytes.push(flags);
|
|
bytes.push(actions.len() as u8);
|
|
bytes.push(0);
|
|
for action in actions {
|
|
bytes.extend_from_slice(action);
|
|
}
|
|
bytes
|
|
}
|
|
|
|
fn encode_action_set_world_flag(key: &str, value: bool) -> Vec<u8> {
|
|
let mut bytes = vec![0x01];
|
|
bytes.extend_from_slice(&encode_len_prefixed_string(key));
|
|
bytes.push(u8::from(value));
|
|
bytes
|
|
}
|
|
|
|
fn encode_action_set_special_condition(label: &str, value: u32) -> Vec<u8> {
|
|
let mut bytes = vec![0x05];
|
|
bytes.extend_from_slice(&encode_len_prefixed_string(label));
|
|
bytes.extend_from_slice(&value.to_le_bytes());
|
|
bytes
|
|
}
|
|
|
|
fn encode_action_adjust_company_cash_ids(ids: &[u32], delta: i64) -> Vec<u8> {
|
|
let mut bytes = vec![0x02, 0x01, ids.len() as u8];
|
|
for id in ids {
|
|
bytes.extend_from_slice(&id.to_le_bytes());
|
|
}
|
|
bytes.extend_from_slice(&delta.to_le_bytes());
|
|
bytes
|
|
}
|
|
|
|
fn encode_action_append_template(template: Vec<u8>) -> Vec<u8> {
|
|
let mut bytes = vec![0x06];
|
|
bytes.extend_from_slice(&(template.len() as u32).to_le_bytes());
|
|
bytes.extend_from_slice(&template);
|
|
bytes
|
|
}
|
|
|
|
fn build_synthetic_event_record(
|
|
trigger_kind: u8,
|
|
flags: u8,
|
|
standalone_count: u8,
|
|
grouped_counts: [u8; 4],
|
|
text_bands: [&[u8]; 6],
|
|
actions: &[Vec<u8>],
|
|
) -> Vec<u8> {
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(PACKED_EVENT_RECORD_SYNTHETIC_MAGIC);
|
|
bytes.push(trigger_kind);
|
|
bytes.push(flags);
|
|
bytes.push(standalone_count);
|
|
bytes.push(actions.len() as u8);
|
|
bytes.extend_from_slice(&grouped_counts);
|
|
for band in text_bands {
|
|
bytes.extend_from_slice(&(band.len() as u16).to_le_bytes());
|
|
bytes.extend_from_slice(band);
|
|
}
|
|
for action in actions {
|
|
bytes.extend_from_slice(action);
|
|
}
|
|
bytes
|
|
}
|
|
|
|
fn encode_real_optional_string(text: &str) -> Vec<u8> {
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&(text.len() as u16).to_le_bytes());
|
|
bytes.extend_from_slice(text.as_bytes());
|
|
bytes
|
|
}
|
|
|
|
fn build_real_condition_row(
|
|
raw_condition_id: i32,
|
|
subtype: u8,
|
|
flag_seed: u8,
|
|
candidate_name: Option<&str>,
|
|
) -> Vec<u8> {
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&(raw_condition_id as u32).to_le_bytes());
|
|
bytes.push(subtype);
|
|
while bytes.len() < PACKED_EVENT_REAL_CONDITION_ROW_LEN {
|
|
bytes.push(flag_seed.wrapping_add(bytes.len() as u8));
|
|
}
|
|
match candidate_name {
|
|
Some(text) => bytes.extend_from_slice(&encode_real_optional_string(text)),
|
|
None => bytes.extend_from_slice(&0u16.to_le_bytes()),
|
|
}
|
|
bytes
|
|
}
|
|
|
|
fn build_real_condition_row_with_threshold(
|
|
raw_condition_id: i32,
|
|
subtype: u8,
|
|
threshold: i32,
|
|
candidate_name: Option<&str>,
|
|
) -> Vec<u8> {
|
|
let mut bytes = build_real_condition_row(raw_condition_id, subtype, 0, candidate_name);
|
|
bytes[5..9].copy_from_slice(&threshold.to_le_bytes());
|
|
bytes
|
|
}
|
|
|
|
struct RealGroupedEffectRowSpec<'a> {
|
|
descriptor_id: u32,
|
|
opcode: u8,
|
|
raw_scalar_value: i32,
|
|
value_byte_0x09: u8,
|
|
value_dword_0x0d: u32,
|
|
value_byte_0x11: u8,
|
|
value_byte_0x12: u8,
|
|
value_word_0x14: u16,
|
|
value_word_0x16: u16,
|
|
locomotive_name: Option<&'a str>,
|
|
}
|
|
|
|
fn build_real_grouped_effect_row(spec: RealGroupedEffectRowSpec<'_>) -> Vec<u8> {
|
|
let mut bytes = vec![0; PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN];
|
|
bytes[0..4].copy_from_slice(&spec.descriptor_id.to_le_bytes());
|
|
bytes[4..8].copy_from_slice(&(spec.raw_scalar_value as u32).to_le_bytes());
|
|
bytes[8] = spec.opcode;
|
|
bytes[9] = spec.value_byte_0x09;
|
|
bytes[0x0d..0x11].copy_from_slice(&spec.value_dword_0x0d.to_le_bytes());
|
|
bytes[0x11] = spec.value_byte_0x11;
|
|
bytes[0x12] = spec.value_byte_0x12;
|
|
bytes[0x14..0x16].copy_from_slice(&spec.value_word_0x14.to_le_bytes());
|
|
bytes[0x16..0x18].copy_from_slice(&spec.value_word_0x16.to_le_bytes());
|
|
match spec.locomotive_name {
|
|
Some(text) => bytes.extend_from_slice(&encode_real_optional_string(text)),
|
|
None => bytes.extend_from_slice(&0u16.to_le_bytes()),
|
|
}
|
|
bytes
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct RealCompactControlSpec {
|
|
mode_byte_0x7ef: u8,
|
|
primary_selector_0x7f0: u32,
|
|
grouped_mode_0x7f4: u8,
|
|
one_shot_header_0x7f5: u32,
|
|
modifier_flag_0x7f9: u8,
|
|
modifier_flag_0x7fa: u8,
|
|
grouped_target_scope_ordinals_0x7fb: [u8; PACKED_EVENT_REAL_GROUP_COUNT],
|
|
grouped_scope_checkboxes_0x7ff: [u8; PACKED_EVENT_REAL_GROUP_COUNT],
|
|
summary_toggle_0x800: u8,
|
|
grouped_territory_selectors_0x80f: [i32; PACKED_EVENT_REAL_GROUP_COUNT],
|
|
}
|
|
|
|
fn build_real_compact_control(spec: RealCompactControlSpec) -> Vec<u8> {
|
|
let mut bytes = Vec::with_capacity(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN);
|
|
bytes.push(spec.mode_byte_0x7ef);
|
|
bytes.extend_from_slice(&spec.primary_selector_0x7f0.to_le_bytes());
|
|
bytes.push(spec.grouped_mode_0x7f4);
|
|
bytes.extend_from_slice(&spec.one_shot_header_0x7f5.to_le_bytes());
|
|
bytes.push(spec.modifier_flag_0x7f9);
|
|
bytes.push(spec.modifier_flag_0x7fa);
|
|
bytes.extend_from_slice(&spec.grouped_target_scope_ordinals_0x7fb);
|
|
bytes.extend_from_slice(&spec.grouped_scope_checkboxes_0x7ff);
|
|
bytes.push(spec.summary_toggle_0x800);
|
|
for selector in spec.grouped_territory_selectors_0x80f {
|
|
bytes.extend_from_slice(&selector.to_le_bytes());
|
|
}
|
|
bytes
|
|
}
|
|
|
|
fn build_real_event_record(
|
|
text_bands: [&[u8]; 6],
|
|
compact_control: Option<RealCompactControlSpec>,
|
|
condition_rows: &[Vec<u8>],
|
|
grouped_rows: [&[Vec<u8>]; 4],
|
|
) -> Vec<u8> {
|
|
let mut bytes = Vec::new();
|
|
for band in text_bands {
|
|
bytes.extend_from_slice(&(band.len() as u16).to_le_bytes());
|
|
bytes.extend_from_slice(band);
|
|
}
|
|
if let Some(spec) = compact_control {
|
|
bytes.extend_from_slice(&build_real_compact_control(spec));
|
|
}
|
|
bytes.extend_from_slice(&PACKED_EVENT_REAL_CONDITION_MARKER.to_le_bytes());
|
|
bytes.extend_from_slice(&(condition_rows.len() as u16).to_le_bytes());
|
|
for row in condition_rows {
|
|
bytes.extend_from_slice(row);
|
|
}
|
|
bytes.extend_from_slice(&PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER.to_le_bytes());
|
|
for rows in grouped_rows {
|
|
bytes.extend_from_slice(&(rows.len() as u16).to_le_bytes());
|
|
}
|
|
for rows in grouped_rows {
|
|
for row in rows {
|
|
bytes.extend_from_slice(row);
|
|
}
|
|
}
|
|
bytes
|
|
}
|
|
|
|
#[test]
|
|
fn parses_synthetic_event_runtime_record_summaries_and_actions() {
|
|
let append_template = encode_template(
|
|
99,
|
|
0x0a,
|
|
0x01,
|
|
&[encode_action_set_special_condition("Imported Follow-On", 1)],
|
|
);
|
|
let record_body = build_synthetic_event_record(
|
|
7,
|
|
0x03,
|
|
1,
|
|
[0, 1, 0, 0],
|
|
[b"Alpha", b"", b"", b"", b"", b""],
|
|
&[
|
|
encode_action_set_world_flag("from_packed_root", true),
|
|
encode_action_append_template(append_template),
|
|
],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC);
|
|
bytes.extend_from_slice(&(record_body.len() as u32).to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(summary.decoded_record_count, 1);
|
|
assert_eq!(summary.imported_runtime_record_count, 1);
|
|
assert_eq!(summary.records.len(), 1);
|
|
assert_eq!(summary.records[0].decode_status, "executable");
|
|
assert_eq!(summary.records[0].payload_family, "synthetic_harness");
|
|
assert_eq!(summary.records[0].text_bands[0].preview, "Alpha");
|
|
assert_eq!(summary.records[0].standalone_condition_row_count, 1);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_row_counts,
|
|
vec![0, 1, 0, 0]
|
|
);
|
|
assert_eq!(summary.records[0].decoded_actions.len(), 2);
|
|
match &summary.records[0].decoded_actions[1] {
|
|
RuntimeEffect::AppendEventRecord { record } => {
|
|
assert_eq!(record.record_id, 99);
|
|
assert_eq!(record.trigger_kind, 0x0a);
|
|
}
|
|
other => panic!("unexpected decoded action: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_company_targeted_synthetic_records_as_parity_only() {
|
|
let record_body = build_synthetic_event_record(
|
|
8,
|
|
0x01,
|
|
0,
|
|
[0, 0, 0, 0],
|
|
[b"", b"", b"", b"", b"", b""],
|
|
&[encode_action_adjust_company_cash_ids(&[7], 25)],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(PACKED_EVENT_RECORDS_SYNTHETIC_MAGIC);
|
|
bytes.extend_from_slice(&(record_body.len() as u32).to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(summary.decoded_record_count, 1);
|
|
assert_eq!(summary.imported_runtime_record_count, 1);
|
|
assert_eq!(summary.records[0].decode_status, "executable");
|
|
assert_eq!(summary.records[0].payload_family, "synthetic_harness");
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_real_style_event_runtime_record_with_zero_rows() {
|
|
let record_body = build_real_event_record(
|
|
[b"Alpha", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0x63,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 1,
|
|
modifier_flag_0x7f9: 1,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 1, 2, 3],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 1, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, 10, -1, 22],
|
|
}),
|
|
&[],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(summary.decoded_record_count, 1);
|
|
assert_eq!(summary.imported_runtime_record_count, 0);
|
|
assert_eq!(summary.records[0].decode_status, "parity_only");
|
|
assert_eq!(summary.records[0].payload_family, "real_packed_v1");
|
|
assert_eq!(summary.records[0].trigger_kind, Some(7));
|
|
assert_eq!(summary.records[0].one_shot, Some(true));
|
|
assert_eq!(
|
|
summary.records[0]
|
|
.compact_control
|
|
.as_ref()
|
|
.expect("real compact control should parse")
|
|
.primary_selector_0x7f0,
|
|
0x63
|
|
);
|
|
assert_eq!(summary.records[0].text_bands[0].preview, "Alpha");
|
|
assert_eq!(summary.records[0].standalone_condition_row_count, 0);
|
|
assert_eq!(summary.records[0].standalone_condition_rows.len(), 0);
|
|
assert!(summary.records[0].negative_sentinel_scope.is_none());
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_row_counts,
|
|
vec![0, 0, 0, 0]
|
|
);
|
|
assert_eq!(summary.records[0].grouped_effect_rows.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_real_style_rows_and_side_strings() {
|
|
let condition_row = build_real_condition_row(-1, 4, 0x30, Some("AutoPlant"));
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 2,
|
|
opcode: 8,
|
|
raw_scalar_value: 7,
|
|
value_byte_0x09: 1,
|
|
value_dword_0x0d: 12,
|
|
value_byte_0x11: 2,
|
|
value_byte_0x12: 3,
|
|
value_word_0x14: 24,
|
|
value_word_0x16: 36,
|
|
locomotive_name: Some("Mikado"),
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"Gamma", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 6,
|
|
primary_selector_0x7f0: 0x2a,
|
|
grouped_mode_0x7f4: 1,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 2,
|
|
modifier_flag_0x7fa: 3,
|
|
grouped_target_scope_ordinals_0x7fb: [1, 4, 7, 8],
|
|
grouped_scope_checkboxes_0x7ff: [0, 1, 0, 1],
|
|
summary_toggle_0x800: 0,
|
|
grouped_territory_selectors_0x80f: [11, -1, 33, -1],
|
|
}),
|
|
&[condition_row],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(summary.records[0].standalone_condition_rows.len(), 1);
|
|
assert_eq!(
|
|
summary.records[0]
|
|
.compact_control
|
|
.as_ref()
|
|
.expect("real compact control should parse")
|
|
.grouped_target_scope_ordinals_0x7fb,
|
|
vec![1, 4, 7, 8]
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0].raw_condition_id,
|
|
-1
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.candidate_name
|
|
.as_deref(),
|
|
Some("AutoPlant")
|
|
);
|
|
let negative_sentinel_scope = summary.records[0]
|
|
.negative_sentinel_scope
|
|
.as_ref()
|
|
.expect("negative-sentinel scope summary should decode");
|
|
assert_eq!(
|
|
negative_sentinel_scope.company_test_scope,
|
|
RuntimeCompanyConditionTestScope::SelectedCompanyOnly
|
|
);
|
|
assert_eq!(
|
|
negative_sentinel_scope.player_test_scope,
|
|
RuntimePlayerConditionTestScope::AiPlayersOnly
|
|
);
|
|
assert!(!negative_sentinel_scope.territory_scope_selector_is_0x63);
|
|
assert_eq!(negative_sentinel_scope.source_row_indexes, vec![0]);
|
|
assert_eq!(summary.records[0].grouped_effect_rows.len(), 1);
|
|
assert_eq!(summary.records[0].grouped_effect_rows[0].opcode, 8);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("Company Cash")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0].target_mask_bits,
|
|
Some(0x01)
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0].row_shape,
|
|
"multivalue_scalar"
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.semantic_family
|
|
.as_deref(),
|
|
Some("multivalue_scalar")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.semantic_preview
|
|
.as_deref(),
|
|
Some("Set Company Cash to 7 with aux [2, 3, 24, 36]")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.locomotive_name
|
|
.as_deref(),
|
|
Some("Mikado")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetCompanyCash {
|
|
target: RuntimeCompanyTarget::SelectedCompany,
|
|
value: 7,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_special_condition_descriptor_from_checked_in_metadata() {
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 108,
|
|
opcode: 3,
|
|
raw_scalar_value: 1,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("Use Wartime Cargos")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.parameter_family
|
|
.as_deref(),
|
|
Some("special_condition_scalar")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetSpecialCondition {
|
|
label: "Use Wartime Cargos".to_string(),
|
|
value: 1,
|
|
}]
|
|
);
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_candidate_availability_descriptor_from_checked_in_metadata() {
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 109,
|
|
opcode: 3,
|
|
raw_scalar_value: 1,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("Turbo Diesel Availability")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.parameter_family
|
|
.as_deref(),
|
|
Some("candidate_availability_scalar")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetCandidateAvailability {
|
|
name: "Turbo Diesel".to_string(),
|
|
value: 1,
|
|
}]
|
|
);
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_special_condition_threshold_from_checked_in_world_condition_metadata() {
|
|
let condition_row = build_real_condition_row_with_threshold(3835, 0, 1, None);
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[condition_row],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.metric
|
|
.as_deref(),
|
|
Some("Special Condition: Use Wartime Cargos")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.semantic_family
|
|
.as_deref(),
|
|
Some("world_state_threshold")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_conditions,
|
|
vec![RuntimeCondition::SpecialConditionThreshold {
|
|
label: "Use Wartime Cargos".to_string(),
|
|
comparator: RuntimeConditionComparator::Ge,
|
|
value: 1,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_candidate_availability_threshold_from_checked_in_world_condition_metadata() {
|
|
let condition_row = build_real_condition_row_with_threshold(
|
|
REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID,
|
|
0,
|
|
2,
|
|
Some("Mogul"),
|
|
);
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[condition_row],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.metric
|
|
.as_deref(),
|
|
Some("Candidate Availability: Mogul")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_conditions,
|
|
vec![RuntimeCondition::CandidateAvailabilityThreshold {
|
|
name: "Mogul".to_string(),
|
|
comparator: RuntimeConditionComparator::Ge,
|
|
value: 2,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_economic_status_threshold_from_checked_in_world_condition_metadata() {
|
|
let condition_row = build_real_condition_row_with_threshold(2350, 0, 4, None);
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[condition_row],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.metric
|
|
.as_deref(),
|
|
Some("Economic Status")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_conditions,
|
|
vec![RuntimeCondition::EconomicStatusCodeThreshold {
|
|
comparator: RuntimeConditionComparator::Ge,
|
|
value: 4,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_named_locomotive_availability_threshold_from_checked_in_metadata() {
|
|
let condition_row = build_real_condition_row_with_threshold(
|
|
REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID,
|
|
4,
|
|
42,
|
|
Some("Big Boy"),
|
|
);
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[condition_row],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.metric
|
|
.as_deref(),
|
|
Some("Named Locomotive Availability: Big Boy")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.semantic_family
|
|
.as_deref(),
|
|
Some("world_scalar_threshold")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_conditions,
|
|
vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold {
|
|
name: "Big Boy".to_string(),
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 42,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_named_locomotive_cost_threshold_from_checked_in_metadata() {
|
|
let condition_row = build_real_condition_row_with_threshold(
|
|
REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID,
|
|
0,
|
|
250000,
|
|
Some("Locomotive 1"),
|
|
);
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[condition_row],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.metric
|
|
.as_deref(),
|
|
Some("Named Locomotive Cost: Locomotive 1")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_conditions,
|
|
vec![RuntimeCondition::NamedLocomotiveCostThreshold {
|
|
name: "Locomotive 1".to_string(),
|
|
comparator: RuntimeConditionComparator::Ge,
|
|
value: 250000,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_named_cargo_production_threshold_from_checked_in_metadata() {
|
|
let condition_row = build_real_condition_row_with_threshold(
|
|
REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID,
|
|
4,
|
|
125,
|
|
Some("Cargo Production Slot 1"),
|
|
);
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[condition_row],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.metric
|
|
.as_deref(),
|
|
Some("Cargo Production: Cargo Production Slot 1")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0].recovered_cargo_slot,
|
|
Some(1)
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_conditions,
|
|
vec![RuntimeCondition::CargoProductionSlotThreshold {
|
|
slot: 1,
|
|
label: "Cargo Production Slot 1".to_string(),
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 125,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_world_scalar_thresholds_from_checked_in_metadata() {
|
|
let condition_rows = vec![
|
|
build_real_condition_row_with_threshold(
|
|
REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID,
|
|
0,
|
|
200,
|
|
None,
|
|
),
|
|
build_real_condition_row_with_threshold(
|
|
REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID,
|
|
4,
|
|
125,
|
|
None,
|
|
),
|
|
build_real_condition_row_with_threshold(
|
|
REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID,
|
|
4,
|
|
75,
|
|
None,
|
|
),
|
|
build_real_condition_row_with_threshold(
|
|
REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID,
|
|
4,
|
|
30,
|
|
None,
|
|
),
|
|
build_real_condition_row_with_threshold(
|
|
REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID,
|
|
4,
|
|
18,
|
|
None,
|
|
),
|
|
build_real_condition_row_with_threshold(
|
|
REAL_TERRITORY_ACCESS_COST_CONDITION_ID,
|
|
4,
|
|
750000,
|
|
None,
|
|
),
|
|
];
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&condition_rows,
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.metric
|
|
.as_deref(),
|
|
Some("Cargo Production Total")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.semantic_family
|
|
.as_deref(),
|
|
Some("world_scalar_threshold")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_conditions,
|
|
vec![
|
|
RuntimeCondition::CargoProductionTotalThreshold {
|
|
comparator: RuntimeConditionComparator::Ge,
|
|
value: 200,
|
|
},
|
|
RuntimeCondition::FactoryProductionTotalThreshold {
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 125,
|
|
},
|
|
RuntimeCondition::FarmMineProductionTotalThreshold {
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 75,
|
|
},
|
|
RuntimeCondition::OtherCargoProductionTotalThreshold {
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 30,
|
|
},
|
|
RuntimeCondition::LimitedTrackBuildingAmountThreshold {
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 18,
|
|
},
|
|
RuntimeCondition::TerritoryAccessCostThreshold {
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 750000,
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_world_flag_condition_from_checked_in_world_condition_metadata() {
|
|
let condition_row = build_real_condition_row_with_threshold(2535, 4, 1, None);
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[condition_row],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.metric
|
|
.as_deref(),
|
|
Some("World Flag: Disable Stock Buying and Selling")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].standalone_condition_rows[0]
|
|
.semantic_family
|
|
.as_deref(),
|
|
Some("world_flag_equals")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_conditions,
|
|
vec![RuntimeCondition::WorldFlagEquals {
|
|
key: "world.disable_stock_buying_and_selling".to_string(),
|
|
value: true,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_checked_in_world_scalar_condition_metadata() {
|
|
let named_cargo =
|
|
real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID)
|
|
.expect("named cargo condition metadata should exist");
|
|
assert_eq!(named_cargo.label, "%1 Production");
|
|
|
|
let availability =
|
|
real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID)
|
|
.expect("availability condition metadata should exist");
|
|
assert_eq!(availability.label, "Unknown Loco Available");
|
|
|
|
let cost = real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID)
|
|
.expect("cost condition metadata should exist");
|
|
assert_eq!(cost.label, "Unknown Loco Cost");
|
|
|
|
let cargo = real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID)
|
|
.expect("cargo condition metadata should exist");
|
|
assert_eq!(cargo.label, "All Cargo Production");
|
|
|
|
let factory = real_ordinary_condition_metadata(REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID)
|
|
.expect("factory production condition metadata should exist");
|
|
assert_eq!(factory.label, "All Factory Production");
|
|
|
|
let farm_mine =
|
|
real_ordinary_condition_metadata(REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID)
|
|
.expect("farm/mine production condition metadata should exist");
|
|
assert_eq!(farm_mine.label, "All Farm/Mine Production");
|
|
|
|
let build_limit =
|
|
real_ordinary_condition_metadata(REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID)
|
|
.expect("build-limit condition metadata should exist");
|
|
assert_eq!(build_limit.label, "Limited Track Building Amount");
|
|
|
|
let access_cost = real_ordinary_condition_metadata(REAL_TERRITORY_ACCESS_COST_CONDITION_ID)
|
|
.expect("territory-access-cost condition metadata should exist");
|
|
assert_eq!(access_cost.label, "Access Rights Cost:");
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_checked_in_chairman_and_governance_condition_metadata() {
|
|
let world_variable = real_ordinary_condition_metadata(REAL_WORLD_VARIABLE_1_CONDITION_ID)
|
|
.expect("world-variable condition metadata should exist");
|
|
assert_eq!(world_variable.label, "Game Variable 1");
|
|
|
|
let player_variable = real_ordinary_condition_metadata(REAL_PLAYER_VARIABLE_3_CONDITION_ID)
|
|
.expect("player-variable condition metadata should exist");
|
|
assert_eq!(player_variable.label, "Player Variable 3");
|
|
|
|
let chairman_cash = real_ordinary_condition_metadata(REAL_CHAIRMAN_CASH_CONDITION_ID)
|
|
.expect("chairman cash condition metadata should exist");
|
|
assert_eq!(chairman_cash.label, "Player Cash");
|
|
|
|
let holdings = real_ordinary_condition_metadata(REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID)
|
|
.expect("chairman holdings condition metadata should exist");
|
|
assert_eq!(holdings.label, "Player Stock Value");
|
|
|
|
let net_worth = real_ordinary_condition_metadata(REAL_CHAIRMAN_NET_WORTH_CONDITION_ID)
|
|
.expect("chairman net worth condition metadata should exist");
|
|
assert_eq!(net_worth.label, "Player Net Worth");
|
|
|
|
let purchasing_power =
|
|
real_ordinary_condition_metadata(REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID)
|
|
.expect("chairman purchasing-power condition metadata should exist");
|
|
assert_eq!(purchasing_power.label, "Purchasing Power");
|
|
|
|
let investor_confidence =
|
|
real_ordinary_condition_metadata(REAL_INVESTOR_CONFIDENCE_CONDITION_ID)
|
|
.expect("investor-confidence condition metadata should exist");
|
|
assert_eq!(investor_confidence.label, "Investor Confidence");
|
|
|
|
let credit_rating = real_ordinary_condition_metadata(REAL_CREDIT_RATING_CONDITION_ID)
|
|
.expect("credit-rating condition metadata should exist");
|
|
assert_eq!(credit_rating.label, "Credit Rating");
|
|
|
|
let prime_rate = real_ordinary_condition_metadata(REAL_PRIME_RATE_CONDITION_ID)
|
|
.expect("prime-rate condition metadata should exist");
|
|
assert_eq!(prime_rate.label, "Prime Rate");
|
|
|
|
let management_attitude =
|
|
real_ordinary_condition_metadata(REAL_MANAGEMENT_ATTITUDE_CONDITION_ID)
|
|
.expect("management-attitude condition metadata should exist");
|
|
assert_eq!(management_attitude.label, "Management Attitude");
|
|
|
|
let book_value = real_ordinary_condition_metadata(REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID)
|
|
.expect("book value condition metadata should exist");
|
|
assert_eq!(book_value.label, "Book Value Per Share");
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_world_variable_condition() {
|
|
let row = SmpLoadedPackedEventConditionRowSummary {
|
|
row_index: 0,
|
|
raw_condition_id: REAL_WORLD_VARIABLE_1_CONDITION_ID,
|
|
subtype: 4,
|
|
flag_bytes: {
|
|
let mut bytes = vec![0; 25];
|
|
bytes[0..4].copy_from_slice(&111_i32.to_le_bytes());
|
|
bytes
|
|
},
|
|
candidate_name: None,
|
|
comparator: Some("eq".to_string()),
|
|
metric: Some("Game Variable 1".to_string()),
|
|
semantic_family: Some("numeric_threshold".to_string()),
|
|
semantic_preview: Some("Test Game Variable 1 == 111".to_string()),
|
|
recovered_cargo_slot: None,
|
|
recovered_cargo_class: None,
|
|
requires_candidate_name_binding: false,
|
|
notes: vec![],
|
|
};
|
|
|
|
assert_eq!(
|
|
decode_real_condition_row(&row, None),
|
|
Some(RuntimeCondition::WorldVariableThreshold {
|
|
index: 1,
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 111,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_player_variable_condition_from_selected_player_scope() {
|
|
let row = SmpLoadedPackedEventConditionRowSummary {
|
|
row_index: 0,
|
|
raw_condition_id: REAL_PLAYER_VARIABLE_3_CONDITION_ID,
|
|
subtype: 4,
|
|
flag_bytes: {
|
|
let mut bytes = vec![0; 25];
|
|
bytes[0..4].copy_from_slice(&333_i32.to_le_bytes());
|
|
bytes
|
|
},
|
|
candidate_name: None,
|
|
comparator: Some("eq".to_string()),
|
|
metric: Some("Player Variable 3".to_string()),
|
|
semantic_family: Some("numeric_threshold".to_string()),
|
|
semantic_preview: Some("Test Player Variable 3 == 333".to_string()),
|
|
recovered_cargo_slot: None,
|
|
recovered_cargo_class: None,
|
|
requires_candidate_name_binding: false,
|
|
notes: vec![],
|
|
};
|
|
let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary {
|
|
company_test_scope: RuntimeCompanyConditionTestScope::Disabled,
|
|
player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly,
|
|
territory_scope_selector_is_0x63: false,
|
|
source_row_indexes: vec![0],
|
|
};
|
|
|
|
assert_eq!(
|
|
decode_real_condition_row(&row, Some(&negative_scope)),
|
|
Some(RuntimeCondition::PlayerVariableThreshold {
|
|
target: RuntimePlayerTarget::SelectedPlayer,
|
|
index: 3,
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 333,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_territory_variable_condition_with_world_territory_scope() {
|
|
let row = SmpLoadedPackedEventConditionRowSummary {
|
|
row_index: 0,
|
|
raw_condition_id: REAL_TERRITORY_VARIABLE_4_CONDITION_ID,
|
|
subtype: 4,
|
|
flag_bytes: {
|
|
let mut bytes = vec![0; 25];
|
|
bytes[0..4].copy_from_slice(&444_i32.to_le_bytes());
|
|
bytes
|
|
},
|
|
candidate_name: None,
|
|
comparator: Some("eq".to_string()),
|
|
metric: Some("Territory Variable 4".to_string()),
|
|
semantic_family: Some("numeric_threshold".to_string()),
|
|
semantic_preview: Some("Test Territory Variable 4 == 444".to_string()),
|
|
recovered_cargo_slot: None,
|
|
recovered_cargo_class: None,
|
|
requires_candidate_name_binding: false,
|
|
notes: vec![],
|
|
};
|
|
let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary {
|
|
company_test_scope: RuntimeCompanyConditionTestScope::Disabled,
|
|
player_test_scope: RuntimePlayerConditionTestScope::Disabled,
|
|
territory_scope_selector_is_0x63: true,
|
|
source_row_indexes: vec![0],
|
|
};
|
|
|
|
assert_eq!(
|
|
decode_real_condition_row(&row, Some(&negative_scope)),
|
|
Some(RuntimeCondition::TerritoryVariableThreshold {
|
|
target: RuntimeTerritoryTarget::AllTerritories,
|
|
index: 4,
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 444,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_chairman_cash_condition_from_selected_player_scope() {
|
|
let row = SmpLoadedPackedEventConditionRowSummary {
|
|
row_index: 0,
|
|
raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID,
|
|
subtype: 4,
|
|
flag_bytes: {
|
|
let mut bytes = vec![0; 25];
|
|
bytes[0..4].copy_from_slice(&500_i32.to_le_bytes());
|
|
bytes
|
|
},
|
|
candidate_name: None,
|
|
comparator: Some("eq".to_string()),
|
|
metric: Some("Player Cash".to_string()),
|
|
semantic_family: Some("numeric_threshold".to_string()),
|
|
semantic_preview: Some("Test Player Cash == 500".to_string()),
|
|
recovered_cargo_slot: None,
|
|
recovered_cargo_class: None,
|
|
requires_candidate_name_binding: false,
|
|
notes: vec![],
|
|
};
|
|
let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary {
|
|
company_test_scope: RuntimeCompanyConditionTestScope::Disabled,
|
|
player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly,
|
|
territory_scope_selector_is_0x63: false,
|
|
source_row_indexes: vec![0],
|
|
};
|
|
|
|
assert_eq!(
|
|
decode_real_condition_row(&row, Some(&negative_scope)),
|
|
Some(RuntimeCondition::ChairmanNumericThreshold {
|
|
target: RuntimeChairmanTarget::SelectedChairman,
|
|
metric: RuntimeChairmanMetric::CurrentCash,
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 500,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_book_value_per_share_condition_to_company_metric() {
|
|
let row = SmpLoadedPackedEventConditionRowSummary {
|
|
row_index: 0,
|
|
raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID,
|
|
subtype: 4,
|
|
flag_bytes: {
|
|
let mut bytes = vec![0; 25];
|
|
bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes());
|
|
bytes
|
|
},
|
|
candidate_name: None,
|
|
comparator: Some("eq".to_string()),
|
|
metric: Some("Book Value Per Share".to_string()),
|
|
semantic_family: Some("numeric_threshold".to_string()),
|
|
semantic_preview: Some("Test Book Value Per Share == 2620".to_string()),
|
|
recovered_cargo_slot: None,
|
|
recovered_cargo_class: None,
|
|
requires_candidate_name_binding: false,
|
|
notes: vec![],
|
|
};
|
|
|
|
assert_eq!(
|
|
decode_real_condition_row(&row, None),
|
|
Some(RuntimeCondition::CompanyNumericThreshold {
|
|
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
|
metric: RuntimeCompanyMetric::BookValuePerShare,
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 2620,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_investor_confidence_condition_to_company_metric() {
|
|
let row = SmpLoadedPackedEventConditionRowSummary {
|
|
row_index: 0,
|
|
raw_condition_id: REAL_INVESTOR_CONFIDENCE_CONDITION_ID,
|
|
subtype: 4,
|
|
flag_bytes: {
|
|
let mut bytes = vec![0; 25];
|
|
bytes[0..4].copy_from_slice(&37_i32.to_le_bytes());
|
|
bytes
|
|
},
|
|
candidate_name: None,
|
|
comparator: Some("eq".to_string()),
|
|
metric: Some("Investor Confidence".to_string()),
|
|
semantic_family: Some("numeric_threshold".to_string()),
|
|
semantic_preview: Some("Test Investor Confidence == 37".to_string()),
|
|
recovered_cargo_slot: None,
|
|
recovered_cargo_class: None,
|
|
requires_candidate_name_binding: false,
|
|
notes: vec![],
|
|
};
|
|
|
|
assert_eq!(
|
|
decode_real_condition_row(&row, None),
|
|
Some(RuntimeCondition::CompanyNumericThreshold {
|
|
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
|
metric: RuntimeCompanyMetric::InvestorConfidence,
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 37,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_management_attitude_condition_to_company_metric() {
|
|
let row = SmpLoadedPackedEventConditionRowSummary {
|
|
row_index: 0,
|
|
raw_condition_id: REAL_MANAGEMENT_ATTITUDE_CONDITION_ID,
|
|
subtype: 4,
|
|
flag_bytes: {
|
|
let mut bytes = vec![0; 25];
|
|
bytes[0..4].copy_from_slice(&58_i32.to_le_bytes());
|
|
bytes
|
|
},
|
|
candidate_name: None,
|
|
comparator: Some("eq".to_string()),
|
|
metric: Some("Management Attitude".to_string()),
|
|
semantic_family: Some("numeric_threshold".to_string()),
|
|
semantic_preview: Some("Test Management Attitude == 58".to_string()),
|
|
recovered_cargo_slot: None,
|
|
recovered_cargo_class: None,
|
|
requires_candidate_name_binding: false,
|
|
notes: vec![],
|
|
};
|
|
|
|
assert_eq!(
|
|
decode_real_condition_row(&row, None),
|
|
Some(RuntimeCondition::CompanyNumericThreshold {
|
|
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
|
metric: RuntimeCompanyMetric::ManagementAttitude,
|
|
comparator: RuntimeConditionComparator::Eq,
|
|
value: 58,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_checked_in_world_flag_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(110).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Disable Stock Buying and Selling");
|
|
assert_eq!(metadata.parameter_family, "world_flag_toggle");
|
|
assert_eq!(
|
|
metadata.runtime_key,
|
|
Some("world.disable_stock_buying_and_selling")
|
|
);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_checked_in_credit_rating_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(56).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Credit Rating");
|
|
assert_eq!(metadata.target_mask_bits, 0x0b);
|
|
assert_eq!(metadata.parameter_family, "company_governance_scalar");
|
|
assert_eq!(
|
|
real_grouped_effect_runtime_status_name(metadata.runtime_status),
|
|
"executable"
|
|
);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[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!(
|
|
real_grouped_effect_descriptor_metadata(descriptor_id).is_some(),
|
|
"descriptor {descriptor_id} should be recoverable from the checked-in effect table"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_checked_in_prime_rate_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(57).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Prime Rate");
|
|
assert_eq!(metadata.target_mask_bits, 0x0b);
|
|
assert_eq!(metadata.parameter_family, "company_governance_scalar");
|
|
assert_eq!(
|
|
real_grouped_effect_runtime_status_name(metadata.runtime_status),
|
|
"executable"
|
|
);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_shell_owned_finance_descriptors_from_checked_in_effect_table() {
|
|
let stock_prices =
|
|
real_grouped_effect_descriptor_metadata(55).expect("descriptor metadata should exist");
|
|
assert_eq!(stock_prices.label, "Stock Prices");
|
|
assert_eq!(
|
|
stock_prices.parameter_family,
|
|
"company_finance_shell_scalar"
|
|
);
|
|
assert_eq!(
|
|
real_grouped_effect_runtime_status_name(stock_prices.runtime_status),
|
|
"shell_owned"
|
|
);
|
|
assert!(!stock_prices.executable_in_runtime);
|
|
|
|
let merger_premium =
|
|
real_grouped_effect_descriptor_metadata(58).expect("descriptor metadata should exist");
|
|
assert_eq!(merger_premium.label, "Merger Premium");
|
|
assert_eq!(
|
|
merger_premium.parameter_family,
|
|
"company_finance_shell_scalar"
|
|
);
|
|
assert_eq!(
|
|
real_grouped_effect_runtime_status_name(merger_premium.runtime_status),
|
|
"shell_owned"
|
|
);
|
|
assert!(!merger_premium.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_credit_rating_descriptor_into_company_governance_scalar_effect() {
|
|
let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 56,
|
|
raw_scalar_value: 640,
|
|
opcode: 3,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let record_body = build_real_event_record(
|
|
[b"Gov", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [1, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&[row_bytes], &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("Credit Rating")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.grouped_target_subject
|
|
.as_deref(),
|
|
Some("company")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetCompanyGovernanceScalar {
|
|
target: RuntimeCompanyTarget::SelectedCompany,
|
|
metric: RuntimeCompanyMetric::CreditRating,
|
|
value: 640,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_world_toggle_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(111).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Disable Margin Buying/Short Selling Stock");
|
|
assert_eq!(metadata.parameter_family, "world_flag_toggle");
|
|
assert_eq!(
|
|
runtime_world_flag_key(metadata),
|
|
Some("world.disable_margin_buying_short_selling_stock".to_string())
|
|
);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_limited_track_building_amount_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(122).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Limited Track Building Amount");
|
|
assert_eq!(metadata.parameter_family, "world_track_build_limit_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_late_world_toggle_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(143).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Disable Train Crashes AND Breakdowns");
|
|
assert_eq!(metadata.parameter_family, "world_flag_toggle");
|
|
assert_eq!(
|
|
runtime_world_flag_key(metadata),
|
|
Some("world.disable_train_crashes_and_breakdowns".to_string())
|
|
);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_locomotive_availability_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(250).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Big Boy 4-8-8-4 Availability");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "locomotive_availability_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_upper_band_recovered_locomotive_availability_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(457).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Upper-Band Locomotive Availability Slot 1");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "locomotive_availability_scalar");
|
|
assert_eq!(recovered_locomotive_availability_loco_id(457), None);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_extended_lower_band_locomotive_availability_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(301).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Zephyr Availability");
|
|
assert_eq!(metadata.parameter_family, "locomotive_availability_scalar");
|
|
assert_eq!(recovered_locomotive_availability_loco_id(301), Some(61));
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_recovered_locomotive_availability_row_with_structured_locomotive_id() {
|
|
let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 250,
|
|
raw_scalar_value: 1,
|
|
opcode: 3,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
|
|
let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None)
|
|
.expect("row should parse");
|
|
|
|
assert_eq!(row.descriptor_id, 250);
|
|
assert_eq!(
|
|
row.descriptor_label.as_deref(),
|
|
Some("Big Boy 4-8-8-4 Availability")
|
|
);
|
|
assert_eq!(row.recovered_locomotive_id, Some(10));
|
|
assert_eq!(
|
|
row.parameter_family.as_deref(),
|
|
Some("locomotive_availability_scalar")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_cargo_production_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(230).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Cargo Production Slot 1");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "cargo_production_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_all_cargo_price_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(105).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "All Cargo Prices");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "cargo_price_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_named_cargo_price_slot_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(106).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Alcohol Price");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "cargo_price_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_runtime_variable_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(43).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Company Variable 1");
|
|
assert_eq!(metadata.target_mask_bits, 0x01);
|
|
assert_eq!(metadata.parameter_family, "runtime_variable_scalar");
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_aggregate_cargo_production_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(177).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "All Cargo Production");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "cargo_production_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_grounded_named_cargo_production_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(180).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Alcohol Production");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "cargo_production_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_lower_band_locomotive_cost_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(352).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "2-D-2 Cost");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "locomotive_cost_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert_eq!(recovered_locomotive_cost_loco_id(352), Some(1));
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_upper_band_locomotive_cost_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(475).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Upper-Band Locomotive Cost Slot 1");
|
|
assert_eq!(metadata.parameter_family, "locomotive_cost_scalar");
|
|
assert_eq!(recovered_locomotive_cost_loco_id(475), None);
|
|
assert!(!metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_extended_lower_band_locomotive_cost_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(412).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Zephyr Cost");
|
|
assert_eq!(metadata.parameter_family, "locomotive_cost_scalar");
|
|
assert_eq!(recovered_locomotive_cost_loco_id(412), Some(61));
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_territory_access_cost_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(453).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Territory Access Cost");
|
|
assert_eq!(metadata.target_mask_bits, 0x08);
|
|
assert_eq!(metadata.parameter_family, "territory_access_cost_scalar");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_recovered_locomotive_cost_row_with_structured_locomotive_id() {
|
|
let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 352,
|
|
raw_scalar_value: 250000,
|
|
opcode: 3,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
|
|
let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None)
|
|
.expect("row should parse");
|
|
|
|
assert_eq!(row.descriptor_id, 352);
|
|
assert_eq!(row.descriptor_label.as_deref(), Some("2-D-2 Cost"));
|
|
assert_eq!(row.recovered_locomotive_id, Some(1));
|
|
assert_eq!(
|
|
row.parameter_family.as_deref(),
|
|
Some("locomotive_cost_scalar")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_grounded_named_cargo_production_row_with_label() {
|
|
let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 180,
|
|
raw_scalar_value: 160,
|
|
opcode: 3,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
|
|
let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None)
|
|
.expect("row should parse");
|
|
|
|
assert_eq!(row.descriptor_id, 180);
|
|
assert_eq!(row.descriptor_label.as_deref(), Some("Alcohol Production"));
|
|
assert_eq!(row.recovered_cargo_label.as_deref(), Some("Alcohol"));
|
|
assert_eq!(
|
|
row.parameter_family.as_deref(),
|
|
Some("cargo_production_scalar")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_grounded_named_cargo_price_row_with_label() {
|
|
let row_bytes = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 106,
|
|
raw_scalar_value: 140,
|
|
opcode: 3,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
|
|
let row = parse_real_grouped_effect_row_summary(&row_bytes, 0, 0, None)
|
|
.expect("row should parse");
|
|
|
|
assert_eq!(row.descriptor_id, 106);
|
|
assert_eq!(row.descriptor_label.as_deref(), Some("Alcohol Price"));
|
|
assert_eq!(row.recovered_cargo_label.as_deref(), Some("Alcohol"));
|
|
assert_eq!(row.parameter_family.as_deref(), Some("cargo_price_scalar"));
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_recovered_locomotive_policy_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(454).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "All Steam Locos Avail.");
|
|
assert_eq!(metadata.parameter_family, "world_flag_toggle");
|
|
assert_eq!(
|
|
metadata.runtime_key,
|
|
Some("world.all_steam_locos_available")
|
|
);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_recovered_locomotive_policy_descriptor_from_checked_in_metadata() {
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 454,
|
|
opcode: 0,
|
|
raw_scalar_value: 1,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"Locos", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 6,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("All Steam Locos Avail.")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.parameter_family
|
|
.as_deref(),
|
|
Some("world_flag_toggle")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetWorldFlag {
|
|
key: "world.all_steam_locos_available".to_string(),
|
|
value: true,
|
|
}]
|
|
);
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_up_checked_in_deactivate_player_descriptor_metadata() {
|
|
let metadata =
|
|
real_grouped_effect_descriptor_metadata(14).expect("descriptor metadata should exist");
|
|
|
|
assert_eq!(metadata.label, "Deactivate Player");
|
|
assert_eq!(metadata.parameter_family, "player_lifecycle_toggle");
|
|
assert_eq!(metadata.runtime_key, None);
|
|
assert!(metadata.executable_in_runtime);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_deactivate_player_descriptor_from_checked_in_metadata() {
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 14,
|
|
opcode: 1,
|
|
raw_scalar_value: 1,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"Players", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [1, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("Deactivate Player")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.parameter_family
|
|
.as_deref(),
|
|
Some("player_lifecycle_toggle")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::DeactivatePlayer {
|
|
target: RuntimePlayerTarget::SelectedPlayer,
|
|
}]
|
|
);
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_real_world_flag_descriptor_with_checked_in_runtime_key() {
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 110,
|
|
opcode: 0,
|
|
raw_scalar_value: 1,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("Disable Stock Buying and Selling")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.parameter_family
|
|
.as_deref(),
|
|
Some("world_flag_toggle")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetWorldFlag {
|
|
key: "world.disable_stock_buying_and_selling".to_string(),
|
|
value: true,
|
|
}]
|
|
);
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_recovered_world_toggle_descriptor_family() {
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 131,
|
|
opcode: 0,
|
|
raw_scalar_value: 1,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("Disable Starting Any Companies")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.parameter_family
|
|
.as_deref(),
|
|
Some("world_flag_toggle")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetWorldFlag {
|
|
key: "world.disable_starting_any_companies".to_string(),
|
|
value: true,
|
|
}]
|
|
);
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_limited_track_building_amount_descriptor_family() {
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 122,
|
|
opcode: 3,
|
|
raw_scalar_value: 18,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 6,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("Limited Track Building Amount")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.parameter_family
|
|
.as_deref(),
|
|
Some("world_track_build_limit_scalar")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }]
|
|
);
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_recovered_late_world_toggle_descriptor_family() {
|
|
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 144,
|
|
opcode: 0,
|
|
raw_scalar_value: 1,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
});
|
|
let group0_rows = vec![grouped_row];
|
|
let record_body = build_real_event_record(
|
|
[b"World", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&group0_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.descriptor_label
|
|
.as_deref(),
|
|
Some("AI Ignore Territories At Startup")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.parameter_family
|
|
.as_deref(),
|
|
Some("world_flag_toggle")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].decoded_actions,
|
|
vec![RuntimeEffect::SetWorldFlag {
|
|
key: "world.ai_ignore_territories_at_startup".to_string(),
|
|
value: true,
|
|
}]
|
|
);
|
|
assert!(summary.records[0].executable_import_ready);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_negative_sentinel_scope_modifiers_and_territory_marker() {
|
|
for (value, expected) in [
|
|
(0, RuntimeCompanyConditionTestScope::Disabled),
|
|
(1, RuntimeCompanyConditionTestScope::AllCompanies),
|
|
(2, RuntimeCompanyConditionTestScope::SelectedCompanyOnly),
|
|
(3, RuntimeCompanyConditionTestScope::AiCompaniesOnly),
|
|
(4, RuntimeCompanyConditionTestScope::HumanCompaniesOnly),
|
|
] {
|
|
assert_eq!(decode_company_condition_test_scope(value), Some(expected));
|
|
}
|
|
for (value, expected) in [
|
|
(0, RuntimePlayerConditionTestScope::Disabled),
|
|
(1, RuntimePlayerConditionTestScope::AllPlayers),
|
|
(2, RuntimePlayerConditionTestScope::SelectedPlayerOnly),
|
|
(3, RuntimePlayerConditionTestScope::AiPlayersOnly),
|
|
(4, RuntimePlayerConditionTestScope::HumanPlayersOnly),
|
|
] {
|
|
assert_eq!(decode_player_condition_test_scope(value), Some(expected));
|
|
}
|
|
|
|
let rows = vec![SmpLoadedPackedEventConditionRowSummary {
|
|
row_index: 0,
|
|
raw_condition_id: -1,
|
|
subtype: 4,
|
|
flag_bytes: vec![0x30; 25],
|
|
candidate_name: Some("AutoPlant".to_string()),
|
|
comparator: None,
|
|
metric: None,
|
|
semantic_family: None,
|
|
semantic_preview: None,
|
|
recovered_cargo_slot: None,
|
|
recovered_cargo_class: None,
|
|
requires_candidate_name_binding: false,
|
|
notes: vec![],
|
|
}];
|
|
let summary = derive_negative_sentinel_scope_summary(
|
|
&rows,
|
|
&SmpLoadedPackedEventCompactControlSummary {
|
|
mode_byte_0x7ef: 6,
|
|
primary_selector_0x7f0: 0x63,
|
|
grouped_mode_0x7f4: 2,
|
|
one_shot_header_0x7f5: 1,
|
|
modifier_flag_0x7f9: 4,
|
|
modifier_flag_0x7fa: 2,
|
|
grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3],
|
|
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
|
|
summary_toggle_0x800: 1,
|
|
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
|
|
},
|
|
)
|
|
.expect("negative sentinel summary should derive");
|
|
assert_eq!(
|
|
summary.company_test_scope,
|
|
RuntimeCompanyConditionTestScope::HumanCompaniesOnly
|
|
);
|
|
assert_eq!(
|
|
summary.player_test_scope,
|
|
RuntimePlayerConditionTestScope::SelectedPlayerOnly
|
|
);
|
|
assert!(summary.territory_scope_selector_is_0x63);
|
|
assert_eq!(summary.source_row_indexes, vec![0]);
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_real_grouped_row_semantic_families() {
|
|
let grouped_rows = vec![
|
|
build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 2,
|
|
opcode: 1,
|
|
raw_scalar_value: 1,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
}),
|
|
build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 2,
|
|
opcode: 4,
|
|
raw_scalar_value: 25,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 2,
|
|
value_word_0x16: 6,
|
|
locomotive_name: None,
|
|
}),
|
|
build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 2,
|
|
opcode: 3,
|
|
raw_scalar_value: 250,
|
|
value_byte_0x09: 0,
|
|
value_dword_0x0d: 0,
|
|
value_byte_0x11: 0,
|
|
value_byte_0x12: 0,
|
|
value_word_0x14: 0,
|
|
value_word_0x16: 0,
|
|
locomotive_name: None,
|
|
}),
|
|
build_real_grouped_effect_row(RealGroupedEffectRowSpec {
|
|
descriptor_id: 2,
|
|
opcode: 8,
|
|
raw_scalar_value: 7,
|
|
value_byte_0x09: 1,
|
|
value_dword_0x0d: 12,
|
|
value_byte_0x11: 2,
|
|
value_byte_0x12: 3,
|
|
value_word_0x14: 24,
|
|
value_word_0x16: 36,
|
|
locomotive_name: Some("Mikado"),
|
|
}),
|
|
];
|
|
let record_body = build_real_event_record(
|
|
[b"Semantic", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 7,
|
|
primary_selector_0x7f0: 0x63,
|
|
grouped_mode_0x7f4: 1,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [1, 1, 1, 1],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 0,
|
|
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
|
|
}),
|
|
&[],
|
|
[&grouped_rows, &[], &[], &[]],
|
|
);
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
let families = summary.records[0]
|
|
.grouped_effect_rows
|
|
.iter()
|
|
.map(|row| row.semantic_family.as_deref().unwrap_or(""))
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
families,
|
|
vec![
|
|
"bool_toggle",
|
|
"timed_duration",
|
|
"scalar_assignment",
|
|
"multivalue_scalar",
|
|
]
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[0]
|
|
.semantic_preview
|
|
.as_deref(),
|
|
Some("Set Company Cash to TRUE")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[1]
|
|
.semantic_preview
|
|
.as_deref(),
|
|
Some("Set Company Cash to 25 for 2 years 6 months")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[2]
|
|
.semantic_preview
|
|
.as_deref(),
|
|
Some("Set Company Cash to 250")
|
|
);
|
|
assert_eq!(
|
|
summary.records[0].grouped_effect_rows[3]
|
|
.semantic_preview
|
|
.as_deref(),
|
|
Some("Set Company Cash to 7 with aux [2, 3, 24, 36]")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_truncated_real_style_event_runtime_record() {
|
|
let mut record_body = build_real_event_record(
|
|
[b"Oops", b"", b"", b"", b"", b""],
|
|
Some(RealCompactControlSpec {
|
|
mode_byte_0x7ef: 5,
|
|
primary_selector_0x7f0: 0,
|
|
grouped_mode_0x7f4: 0,
|
|
one_shot_header_0x7f5: 0,
|
|
modifier_flag_0x7f9: 0,
|
|
modifier_flag_0x7fa: 0,
|
|
grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0],
|
|
grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0],
|
|
summary_toggle_0x800: 0,
|
|
grouped_territory_selectors_0x80f: [0, 0, 0, 0],
|
|
}),
|
|
&[],
|
|
[&[], &[], &[], &[]],
|
|
);
|
|
record_body.pop();
|
|
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
|
|
let header_words = [1u32, 4, 0, 0, 1, 1, 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(&[0x00, 0x00]);
|
|
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
|
|
bytes.extend_from_slice(&record_body);
|
|
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.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");
|
|
|
|
assert_eq!(summary.records[0].decode_status, "unsupported_framing");
|
|
assert_eq!(summary.records[0].payload_family, "unsupported_framing");
|
|
}
|
|
|
|
#[test]
|
|
fn loads_event_runtime_collection_summary_from_report() {
|
|
let mut report = inspect_smp_bytes(&[]);
|
|
let classic_probe = SmpClassicRehydrateProfileProbe {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
progress_32dc_offset: 0x76e8,
|
|
progress_3714_offset: 0x76ec,
|
|
progress_3715_offset: 0x77f8,
|
|
packed_profile_offset: 0x76f0,
|
|
packed_profile_len: 0x108,
|
|
packed_profile_len_hex: "0x108".to_string(),
|
|
packed_profile_block: SmpClassicPackedProfileBlock {
|
|
relative_len: 0x108,
|
|
relative_len_hex: "0x108".to_string(),
|
|
leading_word_0: 3,
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
|
trailing_zero_word_count_after_leading_word: 3,
|
|
map_path_offset: 0x13,
|
|
map_path: Some("British Isles.gmp".to_string()),
|
|
display_name_offset: 0x46,
|
|
display_name: Some("British Isles".to_string()),
|
|
profile_byte_0x77: 0,
|
|
profile_byte_0x77_hex: "0x00".to_string(),
|
|
profile_byte_0x82: 0,
|
|
profile_byte_0x82_hex: "0x00".to_string(),
|
|
profile_byte_0x97: 0,
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
|
profile_byte_0xc5: 0,
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
|
stable_nonzero_words: vec![],
|
|
},
|
|
ascii_runs: vec![],
|
|
};
|
|
report.classic_rehydrate_profile_probe = Some(classic_probe.clone());
|
|
report.save_load_summary = build_save_load_summary(
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
None,
|
|
None,
|
|
Some(&classic_probe),
|
|
None,
|
|
None,
|
|
);
|
|
report.event_runtime_collection_summary = Some(SmpLoadedEventRuntimeCollectionSummary {
|
|
source_kind: "packed-event-runtime-collection".to_string(),
|
|
mechanism_family: "classic-save-rehydrate-v1".to_string(),
|
|
mechanism_confidence: "grounded".to_string(),
|
|
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
|
metadata_tag_offset: 0x7100,
|
|
records_tag_offset: 0x7200,
|
|
close_tag_offset: 0x7600,
|
|
packed_state_version: 0x3e9,
|
|
packed_state_version_hex: "0x000003e9".to_string(),
|
|
live_id_bound: 5,
|
|
live_record_count: 3,
|
|
live_entry_ids: vec![1, 3, 5],
|
|
decoded_record_count: 0,
|
|
imported_runtime_record_count: 0,
|
|
records: build_unsupported_event_runtime_record_summaries(&[1, 3, 5], "test summary"),
|
|
});
|
|
|
|
let slice = load_save_slice_from_report(&report).expect("classic save slice");
|
|
assert_eq!(
|
|
slice
|
|
.event_runtime_collection
|
|
.as_ref()
|
|
.map(|summary| summary.live_entry_ids.clone()),
|
|
Some(vec![1, 3, 5])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn loads_rt3_105_save_slice_from_report() {
|
|
let mut report = inspect_smp_bytes(&[]);
|
|
let packed_profile = SmpRt3105PackedProfileProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
packed_profile_offset: 0x73c0,
|
|
packed_profile_len: 0x108,
|
|
packed_profile_len_hex: "0x108".to_string(),
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
|
relative_len: 0x108,
|
|
relative_len_hex: "0x108".to_string(),
|
|
leading_word_0: 3,
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
|
trailing_zero_word_count_after_leading_word: 2,
|
|
header_flag_word_3: 1,
|
|
header_flag_word_3_hex: "0x00000001".to_string(),
|
|
map_path_offset: 0x10,
|
|
map_path: Some("Alternate USA.gmp".to_string()),
|
|
display_name_offset: 0x43,
|
|
display_name: Some("Alternate USA".to_string()),
|
|
profile_byte_0x77: 0x07,
|
|
profile_byte_0x77_hex: "0x07".to_string(),
|
|
profile_byte_0x82: 0x4d,
|
|
profile_byte_0x82_hex: "0x4d".to_string(),
|
|
profile_byte_0x97: 0,
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
|
profile_byte_0xc5: 0,
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
|
stable_nonzero_words: vec![],
|
|
},
|
|
ascii_runs: vec![],
|
|
};
|
|
let name_table = SmpRt3105SaveNameTableProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-bridge-secondary-block".to_string(),
|
|
semantic_family: "scenario-named-candidate-availability-table".to_string(),
|
|
semantic_alignment: vec![],
|
|
header_offset: 0x6a70,
|
|
header_word_0: 0,
|
|
header_word_0_hex: "0x00000000".to_string(),
|
|
header_word_1: 0,
|
|
header_word_1_hex: "0x00000000".to_string(),
|
|
header_word_2: 0x332e,
|
|
header_word_2_hex: "0x0000332e".to_string(),
|
|
entry_stride: 0x22,
|
|
entry_stride_hex: "0x22".to_string(),
|
|
header_prefix_word_count: 11,
|
|
observed_entry_capacity: 0x44,
|
|
observed_entry_count: 2,
|
|
zero_trailer_entry_count: 1,
|
|
nonzero_trailer_entry_count: 1,
|
|
distinct_trailer_words: vec![0, 1],
|
|
distinct_trailer_hex_words: vec!["0x00000000".to_string(), "0x00000001".to_string()],
|
|
zero_trailer_entry_names: vec!["Uranium Mine".to_string()],
|
|
entries_offset: 0x6ad1,
|
|
entries_end_offset: 0x6b15,
|
|
trailing_footer_hex: "dc3200001437000000".to_string(),
|
|
footer_progress_word_0: 0x32dc,
|
|
footer_progress_word_0_hex: "0x000032dc".to_string(),
|
|
footer_progress_word_1: 0x3714,
|
|
footer_progress_word_1_hex: "0x00003714".to_string(),
|
|
footer_trailing_byte: 0,
|
|
footer_trailing_byte_hex: "0x00".to_string(),
|
|
footer_grounded_alignments: vec![],
|
|
entries: vec![
|
|
SmpRt3105SaveNameTableEntry {
|
|
index: 0,
|
|
offset: 0x6ad1,
|
|
text: "AutoPlant".to_string(),
|
|
availability_dword: 1,
|
|
availability_dword_hex: "0x00000001".to_string(),
|
|
trailer_word: 1,
|
|
trailer_word_hex: "0x00000001".to_string(),
|
|
},
|
|
SmpRt3105SaveNameTableEntry {
|
|
index: 1,
|
|
offset: 0x6af3,
|
|
text: "Uranium Mine".to_string(),
|
|
availability_dword: 0,
|
|
availability_dword_hex: "0x00000000".to_string(),
|
|
trailer_word: 0,
|
|
trailer_word_hex: "0x00000000".to_string(),
|
|
},
|
|
],
|
|
evidence: vec![],
|
|
};
|
|
let named_locomotive_table = SmpRt3105SaveNamedLocomotiveAvailabilityProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-direct-locomotive-row-run".to_string(),
|
|
semantic_family: "scenario-named-locomotive-availability-table".to_string(),
|
|
semantic_alignment: vec![],
|
|
entries_offset: 0x7c78,
|
|
entry_stride: RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE,
|
|
entry_stride_hex: format!("0x{:x}", RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE),
|
|
observed_entry_count: 2,
|
|
zero_availability_count: 1,
|
|
zero_availability_names: vec!["Big Boy".to_string()],
|
|
entries_end_offset: 0x7c78 + 2 * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE,
|
|
entries: vec![
|
|
SmpRt3105SaveNameTableEntry {
|
|
index: 0,
|
|
offset: 0x7c78,
|
|
text: "Big Boy".to_string(),
|
|
availability_dword: 0,
|
|
availability_dword_hex: "0x00000000".to_string(),
|
|
trailer_word: 0,
|
|
trailer_word_hex: "0x00000000".to_string(),
|
|
},
|
|
SmpRt3105SaveNameTableEntry {
|
|
index: 1,
|
|
offset: 0x7cb9,
|
|
text: "GP7".to_string(),
|
|
availability_dword: 1,
|
|
availability_dword_hex: "0x00000001".to_string(),
|
|
trailer_word: 1,
|
|
trailer_word_hex: "0x00000001".to_string(),
|
|
},
|
|
],
|
|
evidence: vec![],
|
|
};
|
|
let bridge = SmpRt3105PostSpanBridgeProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
|
bridge_evidence: vec![],
|
|
span_target_offset: 0x3678,
|
|
next_candidate_offset: Some(0x4f14),
|
|
next_candidate_delta_from_span_target: Some(0x189c),
|
|
packed_profile_offset: 0x73c0,
|
|
packed_profile_delta_from_span_target: 0x3d48,
|
|
next_candidate_delta_from_packed_profile: Some(-0x24ac),
|
|
selector_high_u16: 0x7110,
|
|
selector_high_hex: "0x7110".to_string(),
|
|
descriptor_high_u16: 0x7801,
|
|
descriptor_high_hex: "0x7801".to_string(),
|
|
next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515],
|
|
next_candidate_high_hex_words: vec![],
|
|
};
|
|
report.rt3_105_packed_profile_probe = Some(packed_profile.clone());
|
|
report.rt3_105_save_name_table_probe = Some(name_table.clone());
|
|
report.rt3_105_save_named_locomotive_availability_probe =
|
|
Some(named_locomotive_table.clone());
|
|
report.save_load_summary = build_save_load_summary(
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
None,
|
|
Some(&bridge),
|
|
None,
|
|
Some(&packed_profile),
|
|
Some(&name_table),
|
|
);
|
|
|
|
let slice = load_save_slice_from_report(&report).expect("1.05 save slice");
|
|
assert_eq!(slice.mechanism_family, "rt3-105-save-post-span-bridge-v1");
|
|
assert_eq!(
|
|
slice
|
|
.profile
|
|
.as_ref()
|
|
.and_then(|profile| profile.map_path.as_deref()),
|
|
Some("Alternate USA.gmp")
|
|
);
|
|
assert_eq!(
|
|
slice
|
|
.candidate_availability_table
|
|
.as_ref()
|
|
.expect("candidate table")
|
|
.entries[1]
|
|
.text,
|
|
"Uranium Mine"
|
|
);
|
|
assert_eq!(
|
|
slice
|
|
.named_locomotive_availability_table
|
|
.as_ref()
|
|
.expect("named locomotive availability table")
|
|
.entries[1]
|
|
.text,
|
|
"GP7"
|
|
);
|
|
assert_eq!(
|
|
slice
|
|
.locomotive_catalog
|
|
.as_ref()
|
|
.expect("derived locomotive catalog")
|
|
.entries[0]
|
|
.name,
|
|
"Big Boy"
|
|
);
|
|
assert_eq!(
|
|
slice
|
|
.locomotive_catalog
|
|
.as_ref()
|
|
.expect("derived locomotive catalog")
|
|
.entries[1]
|
|
.locomotive_id,
|
|
2
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_save_world_selection_context_probe_from_fixed_world_block() {
|
|
let mut bytes = vec![0u8; 0x8000];
|
|
let chunk_tag_offset = 0x3ceusize;
|
|
let payload_offset = chunk_tag_offset + 4;
|
|
bytes[chunk_tag_offset..chunk_tag_offset + 4]
|
|
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes());
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET
|
|
..payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET + 4]
|
|
.copy_from_slice(&7u32.to_le_bytes());
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET
|
|
..payload_offset
|
|
+ RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET
|
|
+ 4]
|
|
.copy_from_slice(&9u32.to_le_bytes());
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET
|
|
..payload_offset
|
|
+ RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET
|
|
+ RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT]
|
|
.copy_from_slice(&[3, 1, 4, 1, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET] = 1;
|
|
for (slot_index, role_gate) in [2u8, 1, 0, 2].into_iter().enumerate() {
|
|
bytes[payload_offset
|
|
+ RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET
|
|
+ slot_index * RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE] = role_gate;
|
|
}
|
|
let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN;
|
|
bytes[next_chunk_offset..next_chunk_offset + 4]
|
|
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes());
|
|
|
|
let probe = parse_save_world_selection_context_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("selection-context probe should parse");
|
|
|
|
assert_eq!(probe.chunk_tag_offset, chunk_tag_offset);
|
|
assert_eq!(probe.payload_offset, payload_offset);
|
|
assert_eq!(probe.selected_company_id, 7);
|
|
assert_eq!(probe.selected_chairman_profile_id, 9);
|
|
assert_eq!(probe.chairman_slot_selectors[..6], [3, 1, 4, 1, 5, 9]);
|
|
assert_eq!(probe.campaign_override_flag, 1);
|
|
assert_eq!(probe.chairman_role_gate_bytes[..4], [2, 1, 0, 2]);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_save_world_economic_tuning_probe_from_fixed_world_block() {
|
|
let mut bytes = vec![0u8; 0x8000];
|
|
let chunk_tag_offset = 0x3ceusize;
|
|
let payload_offset = chunk_tag_offset + 4;
|
|
bytes[chunk_tag_offset..chunk_tag_offset + 4]
|
|
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes());
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET
|
|
..payload_offset + RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET + 4]
|
|
.copy_from_slice(&1.5f32.to_bits().to_le_bytes());
|
|
for (lane_index, relative_offset) in
|
|
RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS
|
|
.iter()
|
|
.copied()
|
|
.enumerate()
|
|
{
|
|
let value = (lane_index as f32) + 10.25f32;
|
|
bytes[payload_offset + relative_offset..payload_offset + relative_offset + 4]
|
|
.copy_from_slice(&value.to_bits().to_le_bytes());
|
|
}
|
|
let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN;
|
|
bytes[next_chunk_offset..next_chunk_offset + 4]
|
|
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes());
|
|
|
|
let probe = parse_save_world_economic_tuning_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("world economic tuning probe should parse");
|
|
|
|
assert_eq!(probe.chunk_tag_offset, chunk_tag_offset);
|
|
assert_eq!(probe.payload_offset, payload_offset);
|
|
assert_eq!(probe.mirror_lane.relative_offset_hex, "0xbda");
|
|
assert_eq!(probe.mirror_lane.value_f32, 1.5);
|
|
assert_eq!(probe.tuning_lanes.len(), 6);
|
|
assert_eq!(probe.tuning_lanes[0].relative_offset_hex, "0xbde");
|
|
assert_eq!(probe.tuning_lanes[0].value_f32, 10.25);
|
|
assert_eq!(probe.tuning_lanes[5].relative_offset_hex, "0xbf2");
|
|
assert_eq!(probe.tuning_lanes[5].value_f32, 15.25);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_save_world_issue_37_probe_from_fixed_world_block() {
|
|
let mut bytes = vec![0u8; 0x8000];
|
|
let chunk_tag_offset = 0x3ceusize;
|
|
let payload_offset = chunk_tag_offset + 4;
|
|
bytes[chunk_tag_offset..chunk_tag_offset + 4]
|
|
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes());
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET
|
|
..payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_VALUE_RELATIVE_OFFSET + 4]
|
|
.copy_from_slice(&3u32.to_le_bytes());
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET
|
|
..payload_offset + RT3_SAVE_WORLD_BLOCK_ISSUE_0X37_MULTIPLIER_RELATIVE_OFFSET + 4]
|
|
.copy_from_slice(&0.06f32.to_bits().to_le_bytes());
|
|
let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN;
|
|
bytes[next_chunk_offset..next_chunk_offset + 4]
|
|
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes());
|
|
|
|
let probe = parse_save_world_issue_37_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("world issue-0x37 probe should parse");
|
|
|
|
assert_eq!(probe.chunk_tag_offset, chunk_tag_offset);
|
|
assert_eq!(probe.payload_offset, payload_offset);
|
|
assert_eq!(probe.issue_value_lane.relative_offset_hex, "0x29");
|
|
assert_eq!(probe.issue_value_lane.value_i32, 3);
|
|
assert_eq!(probe.issue_37_raw_u8, 3);
|
|
assert_eq!(probe.issue_37_raw_hex, "0x03");
|
|
assert_eq!(probe.issue_38_raw_u8, 0);
|
|
assert_eq!(probe.issue_38_raw_hex, "0x00");
|
|
assert_eq!(probe.issue_39_raw_u8, 0);
|
|
assert_eq!(probe.issue_39_raw_hex, "0x00");
|
|
assert_eq!(probe.issue_3a_raw_u8, 0);
|
|
assert_eq!(probe.issue_3a_raw_hex, "0x00");
|
|
assert_eq!(probe.multiplier_lane.relative_offset_hex, "0x25");
|
|
assert!((probe.multiplier_lane.value_f32 - 0.06).abs() < f32::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_save_world_finance_neighborhood_probe_from_fixed_world_block() {
|
|
let mut bytes = vec![0u8; 0x8000];
|
|
let chunk_tag_offset = 0x3ceusize;
|
|
let payload_offset = chunk_tag_offset + 4;
|
|
bytes[chunk_tag_offset..chunk_tag_offset + 4]
|
|
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_CHUNK_TAG.to_le_bytes());
|
|
for index in 0..RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS {
|
|
let relative_offset =
|
|
RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_ROOT_RELATIVE_OFFSET + index * 4;
|
|
bytes[payload_offset + relative_offset..payload_offset + relative_offset + 4]
|
|
.copy_from_slice(&((index as u32) + 1).to_le_bytes());
|
|
}
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET] = 1;
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET] = 2;
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET] = 3;
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET] = 4;
|
|
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET
|
|
..payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET + 4]
|
|
.copy_from_slice(&2u32.to_le_bytes());
|
|
let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN;
|
|
bytes[next_chunk_offset..next_chunk_offset + 4]
|
|
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes());
|
|
|
|
let probe = parse_save_world_finance_neighborhood_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("world finance neighborhood probe should parse");
|
|
|
|
assert_eq!(probe.chunk_tag_offset, chunk_tag_offset);
|
|
assert_eq!(probe.payload_offset, payload_offset);
|
|
assert_eq!(
|
|
probe.dword_candidates.len(),
|
|
RT3_SAVE_WORLD_BLOCK_FINANCE_NEIGHBORHOOD_WINDOW_LEN_DWORDS
|
|
);
|
|
assert_eq!(
|
|
probe.current_calendar_tuple_word_lane.relative_offset_hex,
|
|
"0xd"
|
|
);
|
|
assert_eq!(probe.packed_year_word_raw_u16, 1);
|
|
assert_eq!(probe.packed_year_word_raw_hex, "0x0001");
|
|
assert_eq!(probe.partial_year_progress_raw_u8, 0);
|
|
assert_eq!(probe.partial_year_progress_raw_hex, "0x00");
|
|
assert_eq!(probe.stock_policy_raw_u8, 1);
|
|
assert_eq!(probe.stock_policy_raw_hex, "0x01");
|
|
assert_eq!(probe.bond_policy_raw_u8, 2);
|
|
assert_eq!(probe.bond_policy_raw_hex, "0x02");
|
|
assert_eq!(probe.bankruptcy_policy_raw_u8, 3);
|
|
assert_eq!(probe.bankruptcy_policy_raw_hex, "0x03");
|
|
assert_eq!(probe.dividend_policy_raw_u8, 4);
|
|
assert_eq!(probe.dividend_policy_raw_hex, "0x04");
|
|
assert_eq!(probe.building_density_growth_setting_lane.raw_u32, 2);
|
|
assert_eq!(
|
|
probe
|
|
.building_density_growth_setting_lane
|
|
.relative_offset_hex,
|
|
"0x4c78"
|
|
);
|
|
assert_eq!(probe.current_calendar_tuple_word_lane.value_i32, 1);
|
|
assert_eq!(
|
|
probe.current_calendar_tuple_word_2_lane.relative_offset_hex,
|
|
"0x11"
|
|
);
|
|
assert_eq!(probe.current_calendar_tuple_word_2_lane.value_i32, 2);
|
|
assert_eq!(probe.absolute_counter_lane.relative_offset_hex, "0x15");
|
|
assert_eq!(probe.absolute_counter_lane.value_i32, 3);
|
|
assert_eq!(
|
|
probe.absolute_counter_mirror_lane.relative_offset_hex,
|
|
"0x19"
|
|
);
|
|
assert_eq!(probe.absolute_counter_mirror_lane.value_i32, 4);
|
|
assert_eq!(
|
|
probe.dword_candidates[0].label,
|
|
"current_calendar_tuple_word"
|
|
);
|
|
assert_eq!(probe.dword_candidates[0].relative_offset_hex, "0xd");
|
|
assert_eq!(probe.dword_candidates[0].value_i32, 1);
|
|
assert_eq!(probe.dword_candidates[6].label, "issue_0x37_multiplier");
|
|
assert_eq!(probe.dword_candidates[6].relative_offset_hex, "0x25");
|
|
assert_eq!(
|
|
probe.dword_candidates[10].label,
|
|
"issue_neighbor_candidate_2"
|
|
);
|
|
assert_eq!(probe.dword_candidates[10].relative_offset_hex, "0x35");
|
|
assert_eq!(probe.dword_candidates[10].value_i32, 11);
|
|
assert_eq!(
|
|
probe.dword_candidates[11].label,
|
|
"finance_neighborhood_word_12"
|
|
);
|
|
assert_eq!(probe.dword_candidates[11].relative_offset_hex, "0x39");
|
|
assert_eq!(probe.dword_candidates[11].value_i32, 12);
|
|
assert_eq!(
|
|
probe.dword_candidates[16].label,
|
|
"finance_neighborhood_word_17"
|
|
);
|
|
assert_eq!(probe.dword_candidates[16].relative_offset_hex, "0x4d");
|
|
assert_eq!(probe.dword_candidates[16].value_i32, 17);
|
|
}
|
|
|
|
#[test]
|
|
fn loads_selection_only_company_and_chairman_context_from_save_world_probe() {
|
|
let mut report = inspect_smp_bytes(&[]);
|
|
report.save_load_summary = Some(SmpSaveLoadSummary {
|
|
file_extension_hint: Some("gms".to_string()),
|
|
container_profile_family: Some("rt3-105-save-container-v1".to_string()),
|
|
mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
|
mechanism_confidence: "mixed".to_string(),
|
|
packed_profile_kind: None,
|
|
packed_profile_family: None,
|
|
packed_profile_offset: None,
|
|
packed_profile_len: None,
|
|
map_path: None,
|
|
display_name: None,
|
|
profile_byte_0x77: None,
|
|
profile_byte_0x77_hex: None,
|
|
profile_byte_0x82: None,
|
|
profile_byte_0x82_hex: None,
|
|
profile_byte_0x97: None,
|
|
profile_byte_0x97_hex: None,
|
|
profile_byte_0xc5: None,
|
|
profile_byte_0xc5_hex: None,
|
|
trailer_family: Some("rt3-105-save-trailer-v1".to_string()),
|
|
bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()),
|
|
candidate_table: None,
|
|
notes: vec![],
|
|
});
|
|
report.save_world_selection_context_probe = Some(SmpSaveWorldSelectionContextProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-selected-company-and-chairman-context".to_string(),
|
|
chunk_tag_offset: 0x3ce,
|
|
payload_offset: 0x3d2,
|
|
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
|
|
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
|
|
selected_company_id_offset: 0x3ef,
|
|
selected_company_id: 1,
|
|
selected_company_id_hex: "0x00000001".to_string(),
|
|
selected_chairman_profile_id_offset: 0x3f3,
|
|
selected_chairman_profile_id: 9,
|
|
selected_chairman_profile_id_hex: "0x00000009".to_string(),
|
|
chairman_slot_selector_offset: 0x455,
|
|
chairman_slot_selectors: vec![3, 1, 4, 1, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
campaign_override_flag_offset: 0x493,
|
|
campaign_override_flag: 1,
|
|
campaign_override_flag_hex: "0x01".to_string(),
|
|
chairman_role_gate_offset: 0xf91,
|
|
chairman_role_gate_bytes: vec![2, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
evidence: vec![],
|
|
});
|
|
report.save_world_issue_37_probe = Some(SmpSaveWorldIssue37Probe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-save-world-issue-0x37".to_string(),
|
|
chunk_tag_offset: 0x3ce,
|
|
payload_offset: 0x3d2,
|
|
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
|
|
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
|
|
issue_37_raw_u8: 3,
|
|
issue_37_raw_hex: "0x03".to_string(),
|
|
issue_38_raw_u8: 1,
|
|
issue_38_raw_hex: "0x01".to_string(),
|
|
issue_39_raw_u8: 2,
|
|
issue_39_raw_hex: "0x02".to_string(),
|
|
issue_3a_raw_u8: 4,
|
|
issue_3a_raw_hex: "0x04".to_string(),
|
|
issue_value_lane: SmpSaveDwordCandidate {
|
|
label: "issue_0x37_value".to_string(),
|
|
relative_offset: 0x29,
|
|
relative_offset_hex: "0x29".to_string(),
|
|
raw_u32: 3,
|
|
raw_u32_hex: "0x00000003".to_string(),
|
|
value_i32: 3,
|
|
value_f32: f32::from_bits(3),
|
|
},
|
|
multiplier_lane: SmpSaveDwordCandidate {
|
|
label: "issue_0x37_multiplier".to_string(),
|
|
relative_offset: 0x25,
|
|
relative_offset_hex: "0x25".to_string(),
|
|
raw_u32: 0x3d75c28f,
|
|
raw_u32_hex: "0x3d75c28f".to_string(),
|
|
value_i32: 1031127695,
|
|
value_f32: 0.06,
|
|
},
|
|
issue_opinion_base_terms_raw_i32: Vec::new(),
|
|
evidence: vec![],
|
|
});
|
|
report.save_world_economic_tuning_probe = Some(SmpSaveWorldEconomicTuningProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-save-world-economic-tuning".to_string(),
|
|
chunk_tag_offset: 0x3ce,
|
|
payload_offset: 0x3d2,
|
|
payload_len: RT3_SAVE_WORLD_BLOCK_LEN,
|
|
payload_len_hex: format!("0x{:x}", RT3_SAVE_WORLD_BLOCK_LEN),
|
|
mirror_lane: SmpSaveDwordCandidate {
|
|
label: "economic_tuning_mirror_lane_0".to_string(),
|
|
relative_offset: 0xbda,
|
|
relative_offset_hex: "0xbda".to_string(),
|
|
raw_u32: 0x3f46d093,
|
|
raw_u32_hex: "0x3f46d093".to_string(),
|
|
value_i32: 1061605523,
|
|
value_f32: 0.7766201,
|
|
},
|
|
tuning_lanes: vec![
|
|
SmpSaveDwordCandidate {
|
|
label: "economic_tuning_lane_0".to_string(),
|
|
relative_offset: 0xbde,
|
|
relative_offset_hex: "0xbde".to_string(),
|
|
raw_u32: 0x3f400000,
|
|
raw_u32_hex: "0x3f400000".to_string(),
|
|
value_i32: 1061158912,
|
|
value_f32: 0.75,
|
|
},
|
|
SmpSaveDwordCandidate {
|
|
label: "economic_tuning_lane_1".to_string(),
|
|
relative_offset: 0xbe2,
|
|
relative_offset_hex: "0xbe2".to_string(),
|
|
raw_u32: 0x3be56042,
|
|
raw_u32_hex: "0x3be56042".to_string(),
|
|
value_i32: 1004888130,
|
|
value_f32: 0.007,
|
|
},
|
|
],
|
|
evidence: vec![],
|
|
});
|
|
report.save_company_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-company-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-company-header-counts".to_string(),
|
|
metadata_tag_offset: 0x1000,
|
|
records_tag_offset: 0x1100,
|
|
close_tag_offset: 0x1200,
|
|
direct_collection_flag: 1,
|
|
direct_collection_flag_hex: "0x00000001".to_string(),
|
|
direct_record_stride: 0x7684,
|
|
direct_record_stride_hex: "0x00007684".to_string(),
|
|
live_id_bound: 5,
|
|
live_id_bound_hex: "0x00000005".to_string(),
|
|
live_record_count: 1,
|
|
live_record_count_hex: "0x00000001".to_string(),
|
|
header_words: vec![1, 0x7684, 5, 5, 5, 1],
|
|
header_hex_words: vec![
|
|
"0x00000001".to_string(),
|
|
"0x00007684".to_string(),
|
|
"0x00000005".to_string(),
|
|
"0x00000005".to_string(),
|
|
"0x00000005".to_string(),
|
|
"0x00000001".to_string(),
|
|
],
|
|
evidence: vec![],
|
|
});
|
|
report.save_chairman_profile_collection_header_probe =
|
|
Some(SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-chairman-profile-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-chairman-profile-header-counts".to_string(),
|
|
metadata_tag_offset: 0x2000,
|
|
records_tag_offset: 0x2100,
|
|
close_tag_offset: 0x2200,
|
|
direct_collection_flag: 1,
|
|
direct_collection_flag_hex: "0x00000001".to_string(),
|
|
direct_record_stride: 0xcab,
|
|
direct_record_stride_hex: "0x00000cab".to_string(),
|
|
live_id_bound: 8,
|
|
live_id_bound_hex: "0x00000008".to_string(),
|
|
live_record_count: 2,
|
|
live_record_count_hex: "0x00000002".to_string(),
|
|
header_words: vec![1, 0xcab, 8, 6, 8, 2],
|
|
header_hex_words: vec![
|
|
"0x00000001".to_string(),
|
|
"0x00000cab".to_string(),
|
|
"0x00000008".to_string(),
|
|
"0x00000006".to_string(),
|
|
"0x00000008".to_string(),
|
|
"0x00000002".to_string(),
|
|
],
|
|
evidence: vec![],
|
|
});
|
|
report.save_train_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-train-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-train-header-counts".to_string(),
|
|
metadata_tag_offset: 0x3000,
|
|
records_tag_offset: 0x3100,
|
|
close_tag_offset: 0x3200,
|
|
direct_collection_flag: 1,
|
|
direct_collection_flag_hex: "0x00000001".to_string(),
|
|
direct_record_stride: 0x1d5,
|
|
direct_record_stride_hex: "0x000001d5".to_string(),
|
|
live_id_bound: 0x32,
|
|
live_id_bound_hex: "0x00000032".to_string(),
|
|
live_record_count: 0x14,
|
|
live_record_count_hex: "0x00000014".to_string(),
|
|
header_words: vec![1, 0x1d5, 0x32, 0x14, 0x32, 0x14],
|
|
header_hex_words: vec![
|
|
"0x00000001".to_string(),
|
|
"0x000001d5".to_string(),
|
|
"0x00000032".to_string(),
|
|
"0x00000014".to_string(),
|
|
"0x00000032".to_string(),
|
|
"0x00000014".to_string(),
|
|
],
|
|
evidence: vec![],
|
|
});
|
|
report.save_train_collection_directory_probe = Some(SmpSaveTrainCollectionDirectoryProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-train-live-directory".to_string(),
|
|
semantic_family: "scenario-save-train-live-directory".to_string(),
|
|
metadata_tag_offset: 0x3000,
|
|
records_tag_offset: 0x3100,
|
|
close_tag_offset: 0x3200,
|
|
directory_root_dword_index: 16,
|
|
directory_entry_dword_count: 3,
|
|
live_record_count: 0x14,
|
|
live_id_bound: 0x32,
|
|
chain_head_live_entry_id: Some(1),
|
|
chain_tail_live_entry_id: Some(20),
|
|
entries: vec![
|
|
SmpSaveTrainCollectionDirectoryEntryProbe {
|
|
live_entry_id: 1,
|
|
payload_relative_offset: 0x2af8,
|
|
payload_relative_offset_hex: "0x00002af8".to_string(),
|
|
payload_absolute_offset: 0x5afc,
|
|
previous_live_entry_id: 0,
|
|
previous_live_entry_id_hex: "0x00000000".to_string(),
|
|
next_live_entry_id: 2,
|
|
next_live_entry_id_hex: "0x00000002".to_string(),
|
|
},
|
|
SmpSaveTrainCollectionDirectoryEntryProbe {
|
|
live_entry_id: 2,
|
|
payload_relative_offset: 0x2ee0,
|
|
payload_relative_offset_hex: "0x00002ee0".to_string(),
|
|
payload_absolute_offset: 0x5ee4,
|
|
previous_live_entry_id: 1,
|
|
previous_live_entry_id_hex: "0x00000001".to_string(),
|
|
next_live_entry_id: 0,
|
|
next_live_entry_id_hex: "0x00000000".to_string(),
|
|
},
|
|
],
|
|
evidence: vec![],
|
|
});
|
|
report.save_region_collection_header_probe = Some(SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-region-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-region-header-counts".to_string(),
|
|
metadata_tag_offset: 0x5000,
|
|
records_tag_offset: 0x5100,
|
|
close_tag_offset: 0x5200,
|
|
direct_collection_flag: 0,
|
|
direct_collection_flag_hex: "0x00000000".to_string(),
|
|
direct_record_stride: 0x06,
|
|
direct_record_stride_hex: "0x00000006".to_string(),
|
|
live_id_bound: 0x96,
|
|
live_id_bound_hex: "0x00000096".to_string(),
|
|
live_record_count: 0x91,
|
|
live_record_count_hex: "0x00000091".to_string(),
|
|
header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x91],
|
|
header_hex_words: vec![
|
|
"0x00000000".to_string(),
|
|
"0x00000006".to_string(),
|
|
"0x0000000a".to_string(),
|
|
"0x00000014".to_string(),
|
|
"0x00000096".to_string(),
|
|
"0x00000091".to_string(),
|
|
],
|
|
evidence: vec![],
|
|
});
|
|
report.save_region_record_triplet_probe = Some(SmpSaveRegionRecordTripletProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-region-record-triplets".to_string(),
|
|
semantic_family: "scenario-save-region-record-triplets".to_string(),
|
|
records_tag_offset: 0x5100,
|
|
close_tag_offset: 0x5200,
|
|
record_count: 2,
|
|
entries: vec![
|
|
SmpSaveRegionRecordTripletEntryProbe {
|
|
record_index: 0,
|
|
name: "Marker09".to_string(),
|
|
name_tag_relative_offset: 0,
|
|
policy_tag_relative_offset: 0x10,
|
|
profile_tag_relative_offset: 0x2e,
|
|
policy_chunk_len: 0x1a,
|
|
profile_chunk_len: 0x40,
|
|
policy_leading_f32_0: 368.0,
|
|
policy_leading_f32_1: 0.0,
|
|
policy_leading_f32_2: 92.0,
|
|
policy_reserved_dwords: vec![0, 0, 0],
|
|
policy_trailing_word: 1,
|
|
policy_trailing_word_hex: "0x0001".to_string(),
|
|
profile_collection: Some(SmpSaveRegionProfileCollectionProbe {
|
|
direct_collection_flag: 1,
|
|
entry_stride: 0x22,
|
|
live_id_bound: 18,
|
|
live_record_count: 17,
|
|
entry_start_relative_offset: 0x4d,
|
|
trailing_padding_len: 2,
|
|
entries: vec![
|
|
SmpSaveRegionProfileEntryProbe {
|
|
entry_index: 0,
|
|
row_relative_offset: 0x4d,
|
|
name: "House".to_string(),
|
|
trailing_weight_f32: 0.2,
|
|
},
|
|
SmpSaveRegionProfileEntryProbe {
|
|
entry_index: 1,
|
|
row_relative_offset: 0x6f,
|
|
name: "Farm Corn".to_string(),
|
|
trailing_weight_f32: 0.2,
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
SmpSaveRegionRecordTripletEntryProbe {
|
|
record_index: 1,
|
|
name: "Marker10".to_string(),
|
|
name_tag_relative_offset: 0x6e,
|
|
policy_tag_relative_offset: 0x7e,
|
|
profile_tag_relative_offset: 0x9c,
|
|
policy_chunk_len: 0x1a,
|
|
profile_chunk_len: 0x20,
|
|
policy_leading_f32_0: 552.0,
|
|
policy_leading_f32_1: 0.0,
|
|
policy_leading_f32_2: 276.0,
|
|
policy_reserved_dwords: vec![0, 0, 0],
|
|
policy_trailing_word: 1,
|
|
policy_trailing_word_hex: "0x0001".to_string(),
|
|
profile_collection: Some(SmpSaveRegionProfileCollectionProbe {
|
|
direct_collection_flag: 1,
|
|
entry_stride: 0x22,
|
|
live_id_bound: 26,
|
|
live_record_count: 24,
|
|
entry_start_relative_offset: 0x50,
|
|
trailing_padding_len: 0,
|
|
entries: vec![SmpSaveRegionProfileEntryProbe {
|
|
entry_index: 0,
|
|
row_relative_offset: 0x50,
|
|
name: "Farm Corn".to_string(),
|
|
trailing_weight_f32: 0.2,
|
|
}],
|
|
}),
|
|
},
|
|
],
|
|
evidence: vec![],
|
|
});
|
|
report.save_placed_structure_collection_header_probe =
|
|
Some(SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-placed-structure-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-placed-structure-header-counts".to_string(),
|
|
metadata_tag_offset: 0x4000,
|
|
records_tag_offset: 0x4100,
|
|
close_tag_offset: 0x4200,
|
|
direct_collection_flag: 0,
|
|
direct_collection_flag_hex: "0x00000000".to_string(),
|
|
direct_record_stride: 0x06,
|
|
direct_record_stride_hex: "0x00000006".to_string(),
|
|
live_id_bound: 0x7ee,
|
|
live_id_bound_hex: "0x000007ee".to_string(),
|
|
live_record_count: 0x7ea,
|
|
live_record_count_hex: "0x000007ea".to_string(),
|
|
header_words: vec![0, 6, 0x0a, 0x14, 0x7ee, 0x7ea],
|
|
header_hex_words: vec![
|
|
"0x00000000".to_string(),
|
|
"0x00000006".to_string(),
|
|
"0x0000000a".to_string(),
|
|
"0x00000014".to_string(),
|
|
"0x000007ee".to_string(),
|
|
"0x000007ea".to_string(),
|
|
],
|
|
evidence: vec![],
|
|
});
|
|
|
|
let slice = load_save_slice_from_report(&report).expect("save slice");
|
|
|
|
let company_roster = slice.company_roster.expect("selection-only company roster");
|
|
assert_eq!(company_roster.observed_entry_count, 1);
|
|
assert_eq!(company_roster.selected_company_id, Some(1));
|
|
assert!(company_roster.entries.is_empty());
|
|
|
|
let chairman_table = slice
|
|
.chairman_profile_table
|
|
.expect("selection-only chairman table");
|
|
assert_eq!(chairman_table.observed_entry_count, 2);
|
|
assert_eq!(chairman_table.selected_chairman_profile_id, Some(9));
|
|
assert!(chairman_table.entries.is_empty());
|
|
let issue_37_state = slice
|
|
.world_issue_37_state
|
|
.expect("world issue-0x37 state should load");
|
|
assert_eq!(issue_37_state.issue_value, 3);
|
|
assert_eq!(issue_37_state.issue_38_value, 1);
|
|
assert_eq!(issue_37_state.issue_39_value, 2);
|
|
assert_eq!(issue_37_state.issue_3a_value, 4);
|
|
assert_eq!(issue_37_state.multiplier_raw_hex, "0x3d75c28f");
|
|
assert_eq!(issue_37_state.multiplier_value_f32_text, "0.060000");
|
|
let tuning_state = slice
|
|
.world_economic_tuning_state
|
|
.expect("world economic tuning state should load");
|
|
assert_eq!(tuning_state.mirror_raw_hex, "0x3f46d093");
|
|
assert_eq!(tuning_state.mirror_value_f32_text, "0.776620");
|
|
assert_eq!(
|
|
tuning_state.lane_value_f32_text,
|
|
vec!["0.750000", "0.007000"]
|
|
);
|
|
|
|
assert!(
|
|
slice
|
|
.notes
|
|
.iter()
|
|
.any(|note| note.contains("selected_company_id=1"))
|
|
);
|
|
assert!(
|
|
slice
|
|
.notes
|
|
.iter()
|
|
.any(|note| note.contains("selected_chairman_profile_id=9"))
|
|
);
|
|
assert!(
|
|
slice
|
|
.notes
|
|
.iter()
|
|
.any(|note| note.contains("grounded issue-0x37 pair: value=3"))
|
|
);
|
|
assert!(
|
|
slice
|
|
.notes
|
|
.iter()
|
|
.any(|note| note.contains("campaign_override_flag=1"))
|
|
);
|
|
assert!(
|
|
slice
|
|
.notes
|
|
.iter()
|
|
.any(|note| note.contains("tagged company header reports live_record_count=1"))
|
|
);
|
|
assert!(slice.notes.iter().any(|note| {
|
|
note.contains("tagged chairman/profile header reports live_record_count=2")
|
|
}));
|
|
assert!(
|
|
slice
|
|
.notes
|
|
.iter()
|
|
.any(|note| note.contains("tagged train header reports live_record_count=20"))
|
|
);
|
|
assert!(slice.notes.iter().any(|note| {
|
|
note.contains("tagged train metadata also exposes a live-entry directory")
|
|
}));
|
|
assert!(
|
|
slice.notes.iter().any(|note| {
|
|
note.contains("tagged region header reports live_record_count=145")
|
|
})
|
|
);
|
|
assert!(slice.notes.iter().any(|note| {
|
|
note.contains(
|
|
"tagged region records also expose 2 repeated 0x55f1/0x55f2/0x55f3 triplets",
|
|
)
|
|
}));
|
|
assert!(slice.notes.iter().any(|note| {
|
|
note.contains("tagged placed-structure header reports live_record_count=2026")
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_company_tagged_collection_header_probe_from_exact_u32_tags() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x180usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x000061a9u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x000061aau32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000061abu32.to_le_bytes());
|
|
let header_words = [
|
|
1u32, 0x7684, 5, 5, 5, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
|
|
let probe = parse_save_company_collection_header_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("company header probe should parse");
|
|
|
|
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
|
|
assert_eq!(probe.records_tag_offset, records_tag_offset);
|
|
assert_eq!(probe.close_tag_offset, close_tag_offset);
|
|
assert_eq!(probe.direct_record_stride, 0x7684);
|
|
assert_eq!(probe.live_id_bound, 5);
|
|
assert_eq!(probe.live_record_count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_chairman_profile_tagged_collection_header_probe_from_exact_u32_tags() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x180usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x00005209u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x0000520au32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
|
|
let header_words = [
|
|
1u32, 0xcab, 8, 6, 8, 2, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
|
|
let probe = parse_save_chairman_profile_collection_header_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("chairman profile header probe should parse");
|
|
|
|
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
|
|
assert_eq!(probe.records_tag_offset, records_tag_offset);
|
|
assert_eq!(probe.close_tag_offset, close_tag_offset);
|
|
assert_eq!(probe.direct_record_stride, 0xcab);
|
|
assert_eq!(probe.live_id_bound, 8);
|
|
assert_eq!(probe.live_record_count, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_train_tagged_collection_header_probe_from_exact_u32_tags() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x180usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x00005209u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x0000520au32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
|
|
let header_words = [
|
|
1u32, 0x1d5, 0x32, 0x14, 0x32, 0x14, 0x14, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
|
|
let probe = parse_save_train_collection_header_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("train header probe should parse");
|
|
|
|
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
|
|
assert_eq!(probe.records_tag_offset, records_tag_offset);
|
|
assert_eq!(probe.close_tag_offset, close_tag_offset);
|
|
assert_eq!(probe.direct_collection_flag, 1);
|
|
assert_eq!(probe.direct_record_stride, 0x1d5);
|
|
assert_eq!(probe.live_id_bound, 0x32);
|
|
assert_eq!(probe.live_record_count, 0x14);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_region_tagged_collection_header_probe_from_marker09_family() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x180usize;
|
|
let close_tag_offset = 0x1c0usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x00005209u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x0000520au32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
|
|
let header_words = [
|
|
0u32, 0x06, 0x0a, 0x14, 0x96, 0x91, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
let marker_offset = records_tag_offset + 4 + 0x20;
|
|
bytes[marker_offset..marker_offset + 8].copy_from_slice(b"Marker09");
|
|
|
|
let probe = parse_save_region_collection_header_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("region header probe should parse");
|
|
|
|
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
|
|
assert_eq!(probe.records_tag_offset, records_tag_offset);
|
|
assert_eq!(probe.close_tag_offset, close_tag_offset);
|
|
assert_eq!(probe.direct_collection_flag, 0);
|
|
assert_eq!(probe.direct_record_stride, 0x06);
|
|
assert_eq!(probe.live_id_bound, 0x96);
|
|
assert_eq!(probe.live_record_count, 0x91);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_train_collection_directory_probe_from_tagged_metadata_triplets() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x180usize;
|
|
let close_tag_offset = 0x1c0usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x00005209u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x0000520au32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
|
|
let header_words = [
|
|
1u32, 0x1d5, 0x32, 0x03, 0x32, 0x03, 0x14, 1, 0, 0, 1, 1, 0x14, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
let triplets = [(0x2af8u32, 0u32, 2u32), (0x2ee0, 1, 3), (0x32c8, 2, 0)];
|
|
for (index, (offset_word, prev, next)) in triplets.into_iter().enumerate() {
|
|
let base = metadata_tag_offset
|
|
+ 4
|
|
+ (SAVE_REGION_COLLECTION_DIRECTORY_ROOT_DWORD_INDEX + index * 3) * 4;
|
|
bytes[base..base + 4].copy_from_slice(&offset_word.to_le_bytes());
|
|
bytes[base + 4..base + 8].copy_from_slice(&prev.to_le_bytes());
|
|
bytes[base + 8..base + 12].copy_from_slice(&next.to_le_bytes());
|
|
}
|
|
|
|
let header_probe = parse_save_train_collection_header_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("train header probe should parse");
|
|
let directory_probe =
|
|
parse_save_train_collection_directory_probe(&bytes, Some(&header_probe))
|
|
.expect("train directory probe should parse");
|
|
|
|
assert_eq!(directory_probe.directory_root_dword_index, 16);
|
|
assert_eq!(directory_probe.live_record_count, 3);
|
|
assert_eq!(directory_probe.chain_head_live_entry_id, Some(1));
|
|
assert_eq!(directory_probe.chain_tail_live_entry_id, Some(3));
|
|
assert_eq!(directory_probe.entries.len(), 3);
|
|
assert_eq!(directory_probe.entries[0].live_entry_id, 1);
|
|
assert_eq!(directory_probe.entries[0].payload_relative_offset, 0x2af8);
|
|
assert_eq!(directory_probe.entries[0].previous_live_entry_id, 0);
|
|
assert_eq!(directory_probe.entries[0].next_live_entry_id, 2);
|
|
assert_eq!(directory_probe.entries[2].live_entry_id, 3);
|
|
assert_eq!(directory_probe.entries[2].previous_live_entry_id, 2);
|
|
assert_eq!(directory_probe.entries[2].next_live_entry_id, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_region_record_triplet_probe_from_marker09_records() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x260usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x00005209u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x0000520au32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
|
|
let mut cursor = records_tag_offset + 4;
|
|
for (name, x, density, y) in [
|
|
("Marker09", 368.0f32, 0.0f32, 92.0f32),
|
|
("Marker10", 552.0f32, 1.5f32, 276.0f32),
|
|
] {
|
|
bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes());
|
|
bytes[cursor + 4] = name.len() as u8;
|
|
bytes[cursor + 5..cursor + 5 + name.len()].copy_from_slice(name.as_bytes());
|
|
cursor += 0x10;
|
|
bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes());
|
|
bytes[cursor + 4..cursor + 8].copy_from_slice(&x.to_bits().to_le_bytes());
|
|
bytes[cursor + 8..cursor + 12].copy_from_slice(&density.to_bits().to_le_bytes());
|
|
bytes[cursor + 12..cursor + 16].copy_from_slice(&y.to_bits().to_le_bytes());
|
|
bytes[cursor + 28..cursor + 30].copy_from_slice(&1u16.to_le_bytes());
|
|
cursor += 0x1e;
|
|
bytes[cursor..cursor + 2]
|
|
.copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes());
|
|
cursor += 0x40;
|
|
}
|
|
|
|
let header_probe = SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-region-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-region-header-counts".to_string(),
|
|
metadata_tag_offset,
|
|
records_tag_offset,
|
|
close_tag_offset,
|
|
direct_collection_flag: 0,
|
|
direct_collection_flag_hex: "0x00000000".to_string(),
|
|
direct_record_stride: 0x06,
|
|
direct_record_stride_hex: "0x00000006".to_string(),
|
|
live_id_bound: 0x96,
|
|
live_id_bound_hex: "0x00000096".to_string(),
|
|
live_record_count: 2,
|
|
live_record_count_hex: "0x00000002".to_string(),
|
|
header_words: vec![0, 6, 0x0a, 0x14, 0x96, 0x02],
|
|
header_hex_words: vec![],
|
|
evidence: vec![],
|
|
};
|
|
let triplet_probe = parse_save_region_record_triplet_probe(&bytes, Some(&header_probe))
|
|
.expect("region triplet probe should parse");
|
|
|
|
assert_eq!(triplet_probe.record_count, 2);
|
|
assert_eq!(triplet_probe.entries[0].name, "Marker09");
|
|
assert_eq!(triplet_probe.entries[0].policy_tag_relative_offset, 0x10);
|
|
assert_eq!(triplet_probe.entries[0].profile_tag_relative_offset, 0x2e);
|
|
assert_eq!(triplet_probe.entries[0].policy_leading_f32_0, 368.0);
|
|
assert_eq!(triplet_probe.entries[0].policy_leading_f32_1, 0.0);
|
|
assert_eq!(triplet_probe.entries[0].policy_leading_f32_2, 92.0);
|
|
assert_eq!(
|
|
triplet_probe.entries[0].policy_reserved_dwords,
|
|
vec![0, 0, 0]
|
|
);
|
|
assert_eq!(triplet_probe.entries[0].policy_trailing_word, 1);
|
|
assert_eq!(triplet_probe.entries[1].name, "Marker10");
|
|
assert_eq!(triplet_probe.entries[1].policy_leading_f32_0, 552.0);
|
|
assert_eq!(triplet_probe.entries[1].policy_leading_f32_1, 1.5);
|
|
assert_eq!(triplet_probe.entries[1].policy_leading_f32_2, 276.0);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_region_profile_collection_probe_from_fixed_name_rows() {
|
|
let mut payload = vec![0u8; 0x80];
|
|
let header_words = [1u32, 0x22, 2, 2, 3, 2, 0, 1];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = index * 4;
|
|
payload[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
let first_row_offset = 0x20usize;
|
|
let first_name = b"House";
|
|
payload[first_row_offset..first_row_offset + first_name.len()].copy_from_slice(first_name);
|
|
payload[first_row_offset + 0x1e..first_row_offset + 0x22]
|
|
.copy_from_slice(&0.2f32.to_bits().to_le_bytes());
|
|
let second_row_offset = first_row_offset + 0x22;
|
|
let second_name = b"Farm Corn";
|
|
payload[second_row_offset..second_row_offset + second_name.len()]
|
|
.copy_from_slice(second_name);
|
|
payload[second_row_offset + 0x1e..second_row_offset + 0x22]
|
|
.copy_from_slice(&0.45f32.to_bits().to_le_bytes());
|
|
|
|
let profile_probe = parse_save_region_profile_collection_probe(&payload)
|
|
.expect("profile collection probe should parse");
|
|
|
|
assert_eq!(profile_probe.direct_collection_flag, 1);
|
|
assert_eq!(profile_probe.entry_stride, 0x22);
|
|
assert_eq!(profile_probe.live_id_bound, 3);
|
|
assert_eq!(profile_probe.live_record_count, 2);
|
|
assert_eq!(profile_probe.entry_start_relative_offset, 0x20);
|
|
assert_eq!(profile_probe.entries.len(), 2);
|
|
assert_eq!(profile_probe.entries[0].name, "House");
|
|
assert_eq!(profile_probe.entries[0].trailing_weight_f32, 0.2);
|
|
assert_eq!(profile_probe.entries[1].name, "Farm Corn");
|
|
assert_eq!(profile_probe.entries[1].trailing_weight_f32, 0.45);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_region_queued_notice_record_probe_from_seeded_node() {
|
|
let mut bytes = vec![0u8; 0x200];
|
|
let node_base_offset = 0x80usize;
|
|
bytes[node_base_offset + 4..node_base_offset + 8]
|
|
.copy_from_slice(&SAVE_REGION_QUEUED_NOTICE_PAYLOAD_SEED.to_le_bytes());
|
|
bytes[node_base_offset + 8..node_base_offset + 12]
|
|
.copy_from_slice(&SAVE_REGION_QUEUED_NOTICE_NODE_KIND.to_le_bytes());
|
|
bytes[node_base_offset + 12..node_base_offset + 16].copy_from_slice(&0u32.to_le_bytes());
|
|
bytes[node_base_offset + 16..node_base_offset + 20].copy_from_slice(&5u32.to_le_bytes());
|
|
bytes[node_base_offset + 20..node_base_offset + 24].copy_from_slice(&1200u32.to_le_bytes());
|
|
bytes[node_base_offset + 24..node_base_offset + 28].copy_from_slice(&(-1i32).to_le_bytes());
|
|
bytes[node_base_offset + 28..node_base_offset + 32].copy_from_slice(&(-1i32).to_le_bytes());
|
|
|
|
let probe = parse_save_region_queued_notice_record_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-region-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-region-header-counts".to_string(),
|
|
metadata_tag_offset: 0,
|
|
records_tag_offset: 0,
|
|
close_tag_offset: 0,
|
|
direct_collection_flag: 0,
|
|
direct_collection_flag_hex: "0x00000000".to_string(),
|
|
direct_record_stride: 0x06,
|
|
direct_record_stride_hex: "0x00000006".to_string(),
|
|
live_id_bound: 0x96,
|
|
live_id_bound_hex: "0x00000096".to_string(),
|
|
live_record_count: 0x91,
|
|
live_record_count_hex: "0x00000091".to_string(),
|
|
header_words: vec![],
|
|
header_hex_words: vec![],
|
|
evidence: vec![],
|
|
}),
|
|
)
|
|
.expect("region queued notice record probe should parse");
|
|
|
|
assert_eq!(probe.entries.len(), 1);
|
|
assert_eq!(probe.entries[0].node_base_offset, node_base_offset);
|
|
assert_eq!(probe.entries[0].payload_seed_dword_hex, "0x005c87a8");
|
|
assert_eq!(probe.entries[0].kind, SAVE_REGION_QUEUED_NOTICE_NODE_KIND);
|
|
assert_eq!(probe.entries[0].region_id, 5);
|
|
assert_eq!(probe.entries[0].amount, 1200);
|
|
assert_eq!(probe.entries[0].trailing_sentinel_i32_0, -1);
|
|
assert_eq!(probe.entries[0].trailing_sentinel_i32_1, -1);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_placed_structure_record_triplet_probe_from_dual_name_rows() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x260usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x000036b1u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x000036b2u32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000036b3u32.to_le_bytes());
|
|
let mut cursor = records_tag_offset + 4;
|
|
for (primary, secondary, lane0, lane1, lane2, lane3, lane4) in [
|
|
(
|
|
"StationA",
|
|
"StationSetA",
|
|
43111.92f32,
|
|
1385.5f32,
|
|
34581.95f32,
|
|
0.0f32,
|
|
5.9760494f32,
|
|
),
|
|
(
|
|
"StationB",
|
|
"StationSetB",
|
|
44000.0f32,
|
|
1200.0f32,
|
|
33000.0f32,
|
|
0.0f32,
|
|
4.5f32,
|
|
),
|
|
] {
|
|
bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes());
|
|
bytes[cursor + 4] = primary.len() as u8;
|
|
bytes[cursor + 5..cursor + 5 + primary.len()].copy_from_slice(primary.as_bytes());
|
|
let second_len_offset = cursor + 5 + primary.len();
|
|
bytes[second_len_offset] = secondary.len() as u8;
|
|
bytes[second_len_offset + 1..second_len_offset + 1 + secondary.len()]
|
|
.copy_from_slice(secondary.as_bytes());
|
|
cursor += 0x19;
|
|
bytes[cursor..cursor + 2].copy_from_slice(&SAVE_REGION_RECORD_POLICY_TAG.to_le_bytes());
|
|
bytes[cursor + 4..cursor + 8].copy_from_slice(&lane0.to_bits().to_le_bytes());
|
|
bytes[cursor + 8..cursor + 12].copy_from_slice(&lane1.to_bits().to_le_bytes());
|
|
bytes[cursor + 12..cursor + 16].copy_from_slice(&lane2.to_bits().to_le_bytes());
|
|
bytes[cursor + 16..cursor + 20].copy_from_slice(&lane3.to_bits().to_le_bytes());
|
|
bytes[cursor + 20..cursor + 24].copy_from_slice(&lane4.to_bits().to_le_bytes());
|
|
bytes[cursor + 28..cursor + 30].copy_from_slice(&0x0101u16.to_le_bytes());
|
|
cursor += 0x1e;
|
|
bytes[cursor..cursor + 2]
|
|
.copy_from_slice(&SAVE_REGION_RECORD_PROFILE_TAG.to_le_bytes());
|
|
bytes[cursor + 4..cursor + 8].copy_from_slice(&0x5dc1u32.to_le_bytes());
|
|
let mut payload_cursor = cursor + 8;
|
|
bytes[payload_cursor] = primary.len() as u8;
|
|
bytes[payload_cursor + 1..payload_cursor + 1 + primary.len()]
|
|
.copy_from_slice(primary.as_bytes());
|
|
payload_cursor += 1 + primary.len();
|
|
bytes[payload_cursor] = 0;
|
|
payload_cursor += 1;
|
|
bytes[payload_cursor] = secondary.len() as u8;
|
|
bytes[payload_cursor + 1..payload_cursor + 1 + secondary.len()]
|
|
.copy_from_slice(secondary.as_bytes());
|
|
payload_cursor += 1 + secondary.len();
|
|
bytes[payload_cursor] = 0;
|
|
payload_cursor += 1;
|
|
bytes[payload_cursor..payload_cursor + 4].copy_from_slice(&0x0e373500u32.to_le_bytes());
|
|
bytes[payload_cursor + 4..payload_cursor + 8].copy_from_slice(&(-1i32).to_le_bytes());
|
|
bytes[payload_cursor + 8..payload_cursor + 12]
|
|
.copy_from_slice(&0x5dc2u32.to_le_bytes());
|
|
cursor += 0x18 + primary.len() + secondary.len();
|
|
}
|
|
|
|
let header_probe = SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-placed-structure-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-placed-structure-header-counts".to_string(),
|
|
metadata_tag_offset,
|
|
records_tag_offset,
|
|
close_tag_offset,
|
|
direct_collection_flag: 0,
|
|
direct_collection_flag_hex: "0x00000000".to_string(),
|
|
direct_record_stride: 0x06,
|
|
direct_record_stride_hex: "0x00000006".to_string(),
|
|
live_id_bound: 3,
|
|
live_id_bound_hex: "0x00000003".to_string(),
|
|
live_record_count: 2,
|
|
live_record_count_hex: "0x00000002".to_string(),
|
|
header_words: vec![0, 6, 0x0a, 0x14, 3, 2],
|
|
header_hex_words: vec![],
|
|
evidence: vec![],
|
|
};
|
|
let triplet_probe =
|
|
parse_save_placed_structure_record_triplet_probe(&bytes, Some(&header_probe))
|
|
.expect("placed-structure triplet probe should parse");
|
|
|
|
assert_eq!(triplet_probe.record_count, 2);
|
|
assert_eq!(triplet_probe.entries[0].primary_name, "StationA");
|
|
assert_eq!(triplet_probe.entries[0].secondary_name, "StationSetA");
|
|
assert_eq!(triplet_probe.entries[0].policy_chunk_len, 0x1a);
|
|
assert_eq!(triplet_probe.entries[0].policy_f32_lane_4, 5.9760494);
|
|
assert_eq!(triplet_probe.entries[0].policy_trailing_word, 0x0101);
|
|
assert_eq!(
|
|
triplet_probe.entries[0].profile_open_marker_hex,
|
|
"0x00005dc1"
|
|
);
|
|
assert_eq!(
|
|
triplet_probe.entries[0].profile_repeated_primary_name,
|
|
"StationA"
|
|
);
|
|
assert_eq!(
|
|
triplet_probe.entries[0].profile_repeated_secondary_name,
|
|
"StationSetA"
|
|
);
|
|
assert_eq!(
|
|
triplet_probe.entries[0].profile_payload_dword_hex,
|
|
"0x0e373500"
|
|
);
|
|
assert_eq!(triplet_probe.entries[0].profile_sentinel_i32, -1);
|
|
assert_eq!(triplet_probe.entries[0].profile_status_kind, "unset");
|
|
assert_eq!(triplet_probe.entries[0].farm_growth_stage_index, None);
|
|
assert_eq!(
|
|
triplet_probe.entries[0].profile_close_marker_hex,
|
|
"0x00005dc2"
|
|
);
|
|
assert_eq!(triplet_probe.entries[1].primary_name, "StationB");
|
|
assert_eq!(triplet_probe.entries[1].secondary_name, "StationSetB");
|
|
assert_eq!(triplet_probe.entries[1].policy_f32_lane_1, 1200.0);
|
|
}
|
|
|
|
#[test]
|
|
fn derives_placed_structure_farm_growth_stage_from_nonnegative_status() {
|
|
assert_eq!(
|
|
derive_save_placed_structure_profile_status("FarmCorn", "FarmSet", 4),
|
|
("farm_growth_stage_bucket", Some(4))
|
|
);
|
|
assert_eq!(
|
|
derive_save_placed_structure_profile_status("StationA", "StationSetA", -1),
|
|
("unset", None)
|
|
);
|
|
assert_eq!(
|
|
derive_save_placed_structure_profile_status("StationA", "StationSetA", 4),
|
|
("opaque_nondefault", None)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_placed_structure_dynamic_side_buffer_probe_from_embedded_name_row() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x220usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x000038a5u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x000038a6u32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000038a7u32.to_le_bytes());
|
|
let header_words = [
|
|
0u32, 0x06, 1000, 500, 1000, 388, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
let payload_offset = records_tag_offset + 4;
|
|
bytes[payload_offset..payload_offset + 4].copy_from_slice(&0x0005d368u32.to_le_bytes());
|
|
bytes[payload_offset + 4..payload_offset + 6].copy_from_slice(&0x0001u16.to_le_bytes());
|
|
bytes[payload_offset + 6] = 0xff;
|
|
let name_tag_offset = payload_offset + 7;
|
|
bytes[name_tag_offset..name_tag_offset + 2]
|
|
.copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes());
|
|
let first_name = "TrackCapST_Cap.3dp";
|
|
let second_name = "Infrastructure";
|
|
bytes[name_tag_offset + 4] = first_name.len() as u8;
|
|
bytes[name_tag_offset + 5..name_tag_offset + 5 + first_name.len()]
|
|
.copy_from_slice(first_name.as_bytes());
|
|
let second_len_offset = name_tag_offset + 5 + first_name.len();
|
|
bytes[second_len_offset] = second_name.len() as u8;
|
|
bytes[second_len_offset + 1..second_len_offset + 1 + second_name.len()]
|
|
.copy_from_slice(second_name.as_bytes());
|
|
|
|
let probe = parse_save_placed_structure_dynamic_side_buffer_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("placed-structure dynamic side-buffer probe should parse");
|
|
|
|
assert_eq!(probe.direct_record_stride, 0x06);
|
|
assert_eq!(probe.live_id_bound, 1000);
|
|
assert_eq!(probe.live_record_count, 388);
|
|
assert_eq!(probe.owner_shared_dword_hex, "0x0005d368");
|
|
assert_eq!(probe.owner_shared_dword_relative_offset, 0);
|
|
assert!(probe.owner_shared_dword_matches_first_compact_prefix_leading_dword);
|
|
assert_eq!(probe.prefix_leading_dword_hex, "0x0005d368");
|
|
assert_eq!(probe.prefix_trailing_word_hex, "0x0001");
|
|
assert_eq!(probe.prefix_separator_byte_hex, "0xff");
|
|
assert_eq!(probe.first_embedded_name_tag_relative_offset, 7);
|
|
assert_eq!(probe.embedded_name_tag_count, 1);
|
|
assert_eq!(probe.decoded_embedded_name_row_count, 1);
|
|
assert_eq!(probe.decoded_embedded_name_row_with_tertiary_name_count, 0);
|
|
assert_eq!(probe.unique_compact_prefix_pattern_count, 1);
|
|
assert_eq!(
|
|
probe.prefix_leading_dword_matching_embedded_profile_tag_count,
|
|
0
|
|
);
|
|
assert_eq!(probe.unique_embedded_name_pair_count, 1);
|
|
assert_eq!(
|
|
probe.first_embedded_primary_name.as_deref(),
|
|
Some("TrackCapST_Cap.3dp")
|
|
);
|
|
assert_eq!(
|
|
probe.first_embedded_secondary_name.as_deref(),
|
|
Some("Infrastructure")
|
|
);
|
|
assert_eq!(probe.first_embedded_tertiary_name.as_deref(), None);
|
|
assert_eq!(probe.compact_prefix_pattern_summaries.len(), 1);
|
|
assert_eq!(
|
|
probe.compact_prefix_pattern_summaries[0].prefix_leading_dword_hex,
|
|
"0x0005d368"
|
|
);
|
|
assert_eq!(probe.compact_prefix_pattern_summaries[0].count, 1);
|
|
assert_eq!(
|
|
probe.compact_prefix_pattern_summaries[0].cap_like_primary_name_count,
|
|
1
|
|
);
|
|
assert_eq!(
|
|
probe.compact_prefix_pattern_summaries[0].section_like_primary_name_count,
|
|
0
|
|
);
|
|
assert_eq!(probe.name_pair_summaries.len(), 1);
|
|
assert_eq!(probe.name_pair_summaries[0].count, 1);
|
|
assert_eq!(
|
|
probe.name_pair_summaries[0].dominant_prefix_leading_dword_hex,
|
|
"0x0005d368"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn summarizes_placed_structure_dynamic_side_buffer_compact_prefix_patterns() {
|
|
let mut bytes = vec![0u8; 0x600];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x320usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x000038a5u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x000038a6u32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000038a7u32.to_le_bytes());
|
|
let header_words = [
|
|
0u32, 0x06, 1000, 500, 1000, 388, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
let mut cursor = records_tag_offset + 4;
|
|
for (leading_dword, primary_name) in [
|
|
(0x000055f3u32, "TunnelSTBrick_Section.3dp"),
|
|
(0x000055f3u32, "TunnelSTBrick_Cap.3dp"),
|
|
(0xff0000ffu32, "TunnelSTBrick_Cap.3dp"),
|
|
] {
|
|
bytes[cursor..cursor + 4].copy_from_slice(&leading_dword.to_le_bytes());
|
|
bytes[cursor + 4..cursor + 6].copy_from_slice(&0x0001u16.to_le_bytes());
|
|
bytes[cursor + 6] = 0xff;
|
|
let name_tag_offset = cursor + 7;
|
|
bytes[name_tag_offset..name_tag_offset + 2]
|
|
.copy_from_slice(&SAVE_REGION_RECORD_NAME_TAG.to_le_bytes());
|
|
let secondary_name = "Infrastructure";
|
|
bytes[name_tag_offset + 4] = primary_name.len() as u8;
|
|
bytes[name_tag_offset + 5..name_tag_offset + 5 + primary_name.len()]
|
|
.copy_from_slice(primary_name.as_bytes());
|
|
let second_len_offset = name_tag_offset + 5 + primary_name.len();
|
|
bytes[second_len_offset] = secondary_name.len() as u8;
|
|
bytes[second_len_offset + 1..second_len_offset + 1 + secondary_name.len()]
|
|
.copy_from_slice(secondary_name.as_bytes());
|
|
cursor = second_len_offset + 1 + secondary_name.len();
|
|
}
|
|
|
|
let probe = parse_save_placed_structure_dynamic_side_buffer_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("placed-structure dynamic side-buffer probe should parse");
|
|
|
|
assert_eq!(probe.embedded_name_tag_count, 3);
|
|
assert_eq!(probe.decoded_embedded_name_row_count, 3);
|
|
assert_eq!(probe.decoded_embedded_name_row_with_tertiary_name_count, 0);
|
|
assert_eq!(probe.unique_compact_prefix_pattern_count, 2);
|
|
assert_eq!(
|
|
probe.prefix_leading_dword_matching_embedded_profile_tag_count,
|
|
2
|
|
);
|
|
assert_eq!(probe.unique_embedded_name_pair_count, 2);
|
|
assert_eq!(probe.compact_prefix_pattern_summaries.len(), 2);
|
|
assert_eq!(
|
|
probe.compact_prefix_pattern_summaries[0].prefix_leading_dword_hex,
|
|
"0x000055f3"
|
|
);
|
|
assert_eq!(probe.compact_prefix_pattern_summaries[0].count, 2);
|
|
assert_eq!(
|
|
probe.compact_prefix_pattern_summaries[0].section_like_primary_name_count,
|
|
1
|
|
);
|
|
assert_eq!(
|
|
probe.compact_prefix_pattern_summaries[0].cap_like_primary_name_count,
|
|
1
|
|
);
|
|
assert!(
|
|
probe.compact_prefix_pattern_summaries[0]
|
|
.prefix_leading_dword_matches_embedded_profile_tag
|
|
);
|
|
assert_eq!(
|
|
probe.compact_prefix_pattern_summaries[1].prefix_leading_dword_hex,
|
|
"0xff0000ff"
|
|
);
|
|
assert_eq!(probe.compact_prefix_pattern_summaries[1].count, 1);
|
|
assert_eq!(probe.name_pair_summaries.len(), 2);
|
|
assert_eq!(probe.name_pair_summaries[0].count, 2);
|
|
assert_eq!(
|
|
probe.name_pair_summaries[0].dominant_prefix_leading_dword_hex,
|
|
"0x000055f3"
|
|
);
|
|
assert_eq!(probe.name_pair_summaries[1].count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_save_len_prefixed_ascii_name_triplet_with_optional_third_name() {
|
|
let bytes = [
|
|
5u8, b'F', b'i', b'r', b's', b't', 0, 6, b'S', b'e', b'c', b'o', b'n', b'd', 0, 5,
|
|
b'T', b'h', b'i', b'r', b'd',
|
|
];
|
|
let parsed = parse_save_len_prefixed_ascii_name_triplet(&bytes)
|
|
.expect("triplet parser should decode three len-prefixed ascii names");
|
|
assert_eq!(parsed.0, "First");
|
|
assert_eq!(parsed.1, "Second");
|
|
assert_eq!(parsed.2.as_deref(), Some("Third"));
|
|
}
|
|
|
|
#[test]
|
|
fn aligns_placed_structure_dynamic_side_buffer_name_pairs_with_triplets() {
|
|
let side_buffer = SmpSavePlacedStructureDynamicSideBufferProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-placed-structure-dynamic-side-buffer-records".to_string(),
|
|
semantic_family: "scenario-save-placed-structure-dynamic-side-buffer-records"
|
|
.to_string(),
|
|
metadata_tag_offset: 0,
|
|
records_tag_offset: 0,
|
|
close_tag_offset: 0,
|
|
records_span_len: 0,
|
|
direct_record_stride: 6,
|
|
direct_record_stride_hex: "0x00000006".to_string(),
|
|
live_id_bound: 1000,
|
|
live_id_bound_hex: "0x000003e8".to_string(),
|
|
live_record_count: 10,
|
|
live_record_count_hex: "0x0000000a".to_string(),
|
|
owner_shared_dword: 0,
|
|
owner_shared_dword_hex: "0x00000000".to_string(),
|
|
owner_shared_dword_relative_offset: 0,
|
|
owner_shared_dword_matches_first_compact_prefix_leading_dword: true,
|
|
prefix_leading_dword: 0,
|
|
prefix_leading_dword_hex: "0x00000000".to_string(),
|
|
prefix_trailing_word: 1,
|
|
prefix_trailing_word_hex: "0x0001".to_string(),
|
|
prefix_separator_byte: 0xff,
|
|
prefix_separator_byte_hex: "0xff".to_string(),
|
|
first_embedded_name_tag_relative_offset: 7,
|
|
embedded_name_tag_count: 3,
|
|
decoded_embedded_name_row_count: 3,
|
|
decoded_embedded_name_row_with_tertiary_name_count: 0,
|
|
unique_compact_prefix_pattern_count: 2,
|
|
prefix_leading_dword_matching_embedded_profile_tag_count: 2,
|
|
unique_embedded_name_pair_count: 2,
|
|
first_embedded_primary_name: Some("TunnelSTBrick_Section.3dp".to_string()),
|
|
first_embedded_secondary_name: Some("Infrastructure".to_string()),
|
|
first_embedded_tertiary_name: None,
|
|
embedded_name_row_samples: vec![],
|
|
compact_prefix_pattern_summaries: vec![],
|
|
name_pair_summaries: vec![
|
|
SmpSavePlacedStructureDynamicSideBufferNamePairSummary {
|
|
primary_name: "TunnelSTBrick_Section.3dp".to_string(),
|
|
secondary_name: "Infrastructure".to_string(),
|
|
count: 2,
|
|
first_name_tag_relative_offset: 7,
|
|
unique_compact_prefix_pattern_count: 1,
|
|
dominant_prefix_leading_dword: 0x55f3,
|
|
dominant_prefix_leading_dword_hex: "0x000055f3".to_string(),
|
|
dominant_prefix_trailing_word: 1,
|
|
dominant_prefix_trailing_word_hex: "0x0001".to_string(),
|
|
dominant_prefix_separator_byte: 0xff,
|
|
dominant_prefix_separator_byte_hex: "0xff".to_string(),
|
|
dominant_prefix_count: 2,
|
|
},
|
|
SmpSavePlacedStructureDynamicSideBufferNamePairSummary {
|
|
primary_name: "BridgeSTWood_Section.3dp".to_string(),
|
|
secondary_name: "Infrastructure".to_string(),
|
|
count: 1,
|
|
first_name_tag_relative_offset: 27,
|
|
unique_compact_prefix_pattern_count: 1,
|
|
dominant_prefix_leading_dword: 0xff000000,
|
|
dominant_prefix_leading_dword_hex: "0xff000000".to_string(),
|
|
dominant_prefix_trailing_word: 1,
|
|
dominant_prefix_trailing_word_hex: "0x0001".to_string(),
|
|
dominant_prefix_separator_byte: 0xff,
|
|
dominant_prefix_separator_byte_hex: "0xff".to_string(),
|
|
dominant_prefix_count: 1,
|
|
},
|
|
],
|
|
evidence: vec![],
|
|
};
|
|
let triplets = SmpSavePlacedStructureRecordTripletProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-placed-structure-record-triplets".to_string(),
|
|
semantic_family: "scenario-save-placed-structure-record-triplets".to_string(),
|
|
records_tag_offset: 0,
|
|
close_tag_offset: 0,
|
|
record_count: 2,
|
|
entries: vec![
|
|
SmpSavePlacedStructureRecordTripletEntryProbe {
|
|
record_index: 0,
|
|
primary_name: "TunnelSTBrick_Section.3dp".to_string(),
|
|
secondary_name: "Infrastructure".to_string(),
|
|
name_tag_relative_offset: 0,
|
|
policy_tag_relative_offset: 0,
|
|
profile_tag_relative_offset: 0,
|
|
policy_chunk_len: 0,
|
|
profile_chunk_len: 0,
|
|
policy_f32_lane_0: 0.0,
|
|
policy_f32_lane_1: 0.0,
|
|
policy_f32_lane_2: 0.0,
|
|
policy_f32_lane_3: 0.0,
|
|
policy_f32_lane_4: 0.0,
|
|
policy_reserved_dword: 0,
|
|
policy_trailing_word: 0,
|
|
policy_trailing_word_hex: "0x0000".to_string(),
|
|
profile_open_marker: 0,
|
|
profile_open_marker_hex: "0x00000000".to_string(),
|
|
profile_repeated_primary_name: "TunnelSTBrick_Section.3dp".to_string(),
|
|
profile_repeated_secondary_name: "Infrastructure".to_string(),
|
|
profile_payload_dword: 0,
|
|
profile_payload_dword_hex: "0x00000000".to_string(),
|
|
profile_sentinel_i32: -1,
|
|
profile_status_kind: "unset".to_string(),
|
|
farm_growth_stage_index: None,
|
|
profile_close_marker: 0,
|
|
profile_close_marker_hex: "0x00000000".to_string(),
|
|
},
|
|
SmpSavePlacedStructureRecordTripletEntryProbe {
|
|
record_index: 1,
|
|
primary_name: "TrackCapST_Cap.3dp".to_string(),
|
|
secondary_name: "Infrastructure".to_string(),
|
|
name_tag_relative_offset: 0,
|
|
policy_tag_relative_offset: 0,
|
|
profile_tag_relative_offset: 0,
|
|
policy_chunk_len: 0,
|
|
profile_chunk_len: 0,
|
|
policy_f32_lane_0: 0.0,
|
|
policy_f32_lane_1: 0.0,
|
|
policy_f32_lane_2: 0.0,
|
|
policy_f32_lane_3: 0.0,
|
|
policy_f32_lane_4: 0.0,
|
|
policy_reserved_dword: 0,
|
|
policy_trailing_word: 0,
|
|
policy_trailing_word_hex: "0x0000".to_string(),
|
|
profile_open_marker: 0,
|
|
profile_open_marker_hex: "0x00000000".to_string(),
|
|
profile_repeated_primary_name: "TrackCapST_Cap.3dp".to_string(),
|
|
profile_repeated_secondary_name: "Infrastructure".to_string(),
|
|
profile_payload_dword: 0,
|
|
profile_payload_dword_hex: "0x00000000".to_string(),
|
|
profile_sentinel_i32: -1,
|
|
profile_status_kind: "unset".to_string(),
|
|
farm_growth_stage_index: None,
|
|
profile_close_marker: 0,
|
|
profile_close_marker_hex: "0x00000000".to_string(),
|
|
},
|
|
],
|
|
evidence: vec![],
|
|
};
|
|
|
|
let alignment =
|
|
summarize_placed_structure_dynamic_side_buffer_alignment(&side_buffer, &triplets);
|
|
|
|
assert_eq!(alignment.unique_side_buffer_name_pair_count, 2);
|
|
assert_eq!(alignment.unique_triplet_name_pair_count, 2);
|
|
assert_eq!(alignment.overlapping_name_pair_count, 1);
|
|
assert_eq!(
|
|
alignment.side_buffer_rows_with_matching_triplet_name_pair_count,
|
|
2
|
|
);
|
|
assert_eq!(
|
|
alignment.side_buffer_rows_without_matching_triplet_name_pair_count,
|
|
1
|
|
);
|
|
assert_eq!(
|
|
alignment.triplet_name_pairs_without_side_buffer_match_count,
|
|
1
|
|
);
|
|
assert_eq!(alignment.matched_name_pair_samples.len(), 1);
|
|
assert_eq!(alignment.unmatched_side_buffer_name_pair_samples.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_placed_structure_tagged_collection_header_probe_from_exact_u32_tags() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x180usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x000036b1u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x000036b2u32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000036b3u32.to_le_bytes());
|
|
let header_words = [
|
|
0u32, 0x06, 0x0a, 0x14, 0x7ee, 0x7ea, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
|
|
let probe = parse_save_placed_structure_collection_header_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("placed structure header probe should parse");
|
|
|
|
assert_eq!(probe.metadata_tag_offset, metadata_tag_offset);
|
|
assert_eq!(probe.records_tag_offset, records_tag_offset);
|
|
assert_eq!(probe.close_tag_offset, close_tag_offset);
|
|
assert_eq!(probe.direct_collection_flag, 0);
|
|
assert_eq!(probe.direct_record_stride, 0x06);
|
|
assert_eq!(probe.live_id_bound, 0x7ee);
|
|
assert_eq!(probe.live_record_count, 0x7ea);
|
|
}
|
|
|
|
#[test]
|
|
fn scans_unclassified_tagged_collection_header_probe_from_adjacent_low_tags() {
|
|
let mut bytes = vec![0u8; 0x400];
|
|
let metadata_tag_offset = 0x40usize;
|
|
let records_tag_offset = 0x140usize;
|
|
let close_tag_offset = 0x1c0usize;
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x00007001u32.to_le_bytes());
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x00007002u32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x00007003u32.to_le_bytes());
|
|
let header_words = [
|
|
0u32, 0x12, 0x0a, 0x14, 0x90, 0x78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
|
|
let probes = scan_save_unclassified_tagged_collection_header_probes(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
);
|
|
|
|
let probe = probes
|
|
.iter()
|
|
.find(|probe| probe.metadata_tag == 0x7001)
|
|
.expect("should include synthetic unclassified tag family");
|
|
assert_eq!(probe.records_tag, 0x7002);
|
|
assert_eq!(probe.close_tag, 0x7003);
|
|
assert_eq!(probe.direct_record_stride, 0x12);
|
|
assert_eq!(probe.live_id_bound, 0x90);
|
|
assert_eq!(probe.live_record_count, 0x78);
|
|
assert_eq!(
|
|
probe.records_span_len,
|
|
close_tag_offset - (records_tag_offset + 4)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_save_company_roster_probe_from_direct_records() {
|
|
let metadata_tag_offset = 0x40usize;
|
|
let stride = 0x7684usize;
|
|
let count = 2usize;
|
|
let start_offset = 0xc6usize;
|
|
let total_len = metadata_tag_offset + 4 + start_offset + stride * count + 0x400;
|
|
let mut bytes = vec![0u8; total_len];
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x000061a9u32.to_le_bytes());
|
|
let header_words = [
|
|
1u32,
|
|
0x7684,
|
|
5,
|
|
5,
|
|
5,
|
|
count as u32,
|
|
1,
|
|
1,
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
1,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
let records_tag_offset = metadata_tag_offset + 4 + 0x200;
|
|
let close_tag_offset = records_tag_offset + 4;
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x000061aau32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x000061abu32.to_le_bytes());
|
|
|
|
for (
|
|
index,
|
|
(
|
|
name,
|
|
linked,
|
|
merger,
|
|
takeover,
|
|
bond_count,
|
|
_debt,
|
|
track_capacity,
|
|
mutable_support_scalar_raw_u32,
|
|
young_company_support_scalar_raw_u32,
|
|
support_progress_word,
|
|
recent_per_share_subscore_raw_u32,
|
|
cached_share_price_raw_u32,
|
|
chairman_salary_baseline,
|
|
chairman_salary_current,
|
|
chairman_bonus_year,
|
|
chairman_bonus_amount,
|
|
founding_year,
|
|
last_bankruptcy_year,
|
|
last_dividend_year,
|
|
current_issue_calendar_word,
|
|
current_issue_calendar_word_2,
|
|
prior_issue_calendar_word,
|
|
prior_issue_calendar_word_2,
|
|
preferred_locomotive_engine_type_raw_u8,
|
|
city_connection_latch,
|
|
linked_transit_latch,
|
|
),
|
|
) in [
|
|
(
|
|
"Company One",
|
|
1u32,
|
|
1862u32,
|
|
1865u32,
|
|
2u8,
|
|
1_000_000u32,
|
|
Some(603i32),
|
|
0x3f800000u32,
|
|
0x42340000u32,
|
|
17u32,
|
|
0x41f00000u32,
|
|
0x426c0000u32,
|
|
24u32,
|
|
31u32,
|
|
1849u32,
|
|
1250i32,
|
|
1842u32,
|
|
1851u32,
|
|
1848u32,
|
|
7u32,
|
|
8u32,
|
|
6u32,
|
|
7u32,
|
|
2u8,
|
|
true,
|
|
false,
|
|
),
|
|
(
|
|
"Company Two",
|
|
2u32,
|
|
0u32,
|
|
1871u32,
|
|
1u8,
|
|
500_000u32,
|
|
None,
|
|
0x40000000u32,
|
|
0x42700000u32,
|
|
33u32,
|
|
0x42000000u32,
|
|
0x42780000u32,
|
|
28u32,
|
|
36u32,
|
|
0u32,
|
|
0i32,
|
|
1845u32,
|
|
0u32,
|
|
1850u32,
|
|
3u32,
|
|
4u32,
|
|
2u32,
|
|
3u32,
|
|
1u8,
|
|
false,
|
|
true,
|
|
),
|
|
]
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
let record_offset = metadata_tag_offset + 4 + start_offset + index * stride;
|
|
bytes[record_offset..record_offset + 4]
|
|
.copy_from_slice(&((index + 1) as u32).to_le_bytes());
|
|
bytes[record_offset + 4..record_offset + 4 + name.len()]
|
|
.copy_from_slice(name.as_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_LINKED_CHAIRMAN_OFFSET + 4]
|
|
.copy_from_slice(&linked.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_ACTIVE_OFFSET] = 1;
|
|
bytes[record_offset + 0x47..record_offset + 0x4b]
|
|
.copy_from_slice(&20000u32.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_SUPPORT_SCALAR_OFFSET + 4]
|
|
.copy_from_slice(&mutable_support_scalar_raw_u32.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_COMPANY_VALUE_OFFSET + 4]
|
|
.copy_from_slice(&young_company_support_scalar_raw_u32.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_BOND_COUNT_OFFSET] = bond_count;
|
|
for slot_index in 0..bond_count as usize {
|
|
let slot_offset = record_offset
|
|
+ SAVE_COMPANY_RECORD_BOND_TABLE_OFFSET
|
|
+ slot_index * SAVE_COMPANY_RECORD_BOND_SLOT_STRIDE;
|
|
let (principal, coupon_rate) = if index == 0 && slot_index == 0 {
|
|
(900_000i32, 0.08f32)
|
|
} else if index == 0 && slot_index == 1 {
|
|
(650_000i32, 0.12f32)
|
|
} else {
|
|
(500_000i32, 0.10f32)
|
|
};
|
|
bytes[slot_offset..slot_offset + 4].copy_from_slice(&principal.to_le_bytes());
|
|
bytes[slot_offset + 4..slot_offset + 8]
|
|
.copy_from_slice(&(1894u32 + slot_index as u32).to_le_bytes());
|
|
bytes[slot_offset + 8..slot_offset + 12]
|
|
.copy_from_slice(&coupon_rate.to_le_bytes());
|
|
}
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_MERGER_COOLDOWN_OFFSET + 4]
|
|
.copy_from_slice(&merger.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_TAKEOVER_COOLDOWN_OFFSET + 4]
|
|
.copy_from_slice(&takeover.to_le_bytes());
|
|
let raw_capacity = track_capacity.unwrap_or(-1);
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_TRACK_LAYING_CAPACITY_OFFSET + 4]
|
|
.copy_from_slice(&raw_capacity.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_SUPPORT_PROGRESS_OFFSET + 4]
|
|
.copy_from_slice(&support_progress_word.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_RECENT_PER_SHARE_SUBSCORE_OFFSET + 4]
|
|
.copy_from_slice(&recent_per_share_subscore_raw_u32.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_CACHED_SHARE_PRICE_OFFSET + 4]
|
|
.copy_from_slice(&cached_share_price_raw_u32.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_BASELINE_OFFSET + 4]
|
|
.copy_from_slice(&chairman_salary_baseline.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_SALARY_CURRENT_OFFSET + 4]
|
|
.copy_from_slice(&chairman_salary_current.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_YEAR_OFFSET + 4]
|
|
.copy_from_slice(&chairman_bonus_year.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_CHAIRMAN_BONUS_AMOUNT_OFFSET + 4]
|
|
.copy_from_slice(&chairman_bonus_amount.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_FOUNDING_YEAR_OFFSET + 4]
|
|
.copy_from_slice(&founding_year.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_LAST_BANKRUPTCY_YEAR_OFFSET + 4]
|
|
.copy_from_slice(&last_bankruptcy_year.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_LAST_DIVIDEND_YEAR_OFFSET + 4]
|
|
.copy_from_slice(&last_dividend_year.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET + 4]
|
|
.copy_from_slice(¤t_issue_calendar_word.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2
|
|
..record_offset + SAVE_COMPANY_RECORD_CURRENT_ISSUE_CALENDAR_OFFSET_2 + 4]
|
|
.copy_from_slice(¤t_issue_calendar_word_2.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET
|
|
..record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET + 4]
|
|
.copy_from_slice(&prior_issue_calendar_word.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2
|
|
..record_offset + SAVE_COMPANY_RECORD_PRIOR_ISSUE_CALENDAR_OFFSET_2 + 4]
|
|
.copy_from_slice(&prior_issue_calendar_word_2.to_le_bytes());
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_PREFERRED_LOCOMOTIVE_ENGINE_TYPE_OFFSET] =
|
|
preferred_locomotive_engine_type_raw_u8;
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_CITY_CONNECTION_LATCH_OFFSET] =
|
|
u8::from(city_connection_latch);
|
|
bytes[record_offset + SAVE_COMPANY_RECORD_LINKED_TRANSIT_LATCH_OFFSET] =
|
|
u8::from(linked_transit_latch);
|
|
let current_cash: f64 = if index == 0 { 125_000.0 } else { -25_000.0 };
|
|
let current_cash_slot_offset = record_offset
|
|
+ SAVE_COMPANY_RECORD_STAT_BAND_ROOT_0D7F_OFFSET
|
|
+ (crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH as usize
|
|
* crate::RUNTIME_COMPANY_YEAR_STAT_FAMILY_SPAN as usize
|
|
* 8);
|
|
bytes[current_cash_slot_offset..current_cash_slot_offset + 8]
|
|
.copy_from_slice(¤t_cash.to_bits().to_le_bytes());
|
|
}
|
|
|
|
let header_probe = parse_save_company_collection_header_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("company header probe should parse");
|
|
let roster = parse_save_company_roster_probe(
|
|
&bytes,
|
|
Some(&header_probe),
|
|
Some(&SmpSaveWorldSelectionContextProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-selected-company-and-chairman-context".to_string(),
|
|
chunk_tag_offset: 0,
|
|
payload_offset: 0,
|
|
payload_len: 0,
|
|
payload_len_hex: "0x0".to_string(),
|
|
selected_company_id_offset: 0,
|
|
selected_company_id: 2,
|
|
selected_company_id_hex: "0x00000002".to_string(),
|
|
selected_chairman_profile_id_offset: 0,
|
|
selected_chairman_profile_id: 1,
|
|
selected_chairman_profile_id_hex: "0x00000001".to_string(),
|
|
chairman_slot_selector_offset: 0,
|
|
chairman_slot_selectors: vec![],
|
|
campaign_override_flag_offset: 0,
|
|
campaign_override_flag: 0,
|
|
campaign_override_flag_hex: "0x00".to_string(),
|
|
chairman_role_gate_offset: 0,
|
|
chairman_role_gate_bytes: vec![],
|
|
evidence: vec![],
|
|
}),
|
|
)
|
|
.expect("company roster should parse");
|
|
|
|
assert_eq!(roster.observed_entry_count, 2);
|
|
assert_eq!(roster.selected_company_id, Some(2));
|
|
assert_eq!(roster.entries.len(), 2);
|
|
assert_eq!(roster.entries[0].company_id, 1);
|
|
assert_eq!(roster.entries[0].current_cash, 125_000);
|
|
assert_eq!(roster.entries[0].linked_chairman_profile_id, Some(1));
|
|
assert_eq!(roster.entries[0].debt, 1_550_000);
|
|
assert_eq!(roster.entries[0].available_track_laying_capacity, Some(603));
|
|
assert_eq!(roster.entries[0].merger_cooldown_year, Some(1862));
|
|
let market_state = roster.entries[0]
|
|
.market_state
|
|
.as_ref()
|
|
.expect("company market state should load");
|
|
assert_eq!(market_state.outstanding_shares, 20_000);
|
|
assert_eq!(market_state.live_bond_slots.len(), 2);
|
|
assert_eq!(market_state.live_bond_slots[0].principal, 900_000);
|
|
assert_eq!(market_state.live_bond_slots[0].maturity_year, 1894);
|
|
assert_eq!(
|
|
market_state.live_bond_slots[1].coupon_rate_raw_u32,
|
|
0.12f32.to_bits()
|
|
);
|
|
assert_eq!(market_state.largest_live_bond_principal, Some(900_000));
|
|
assert_eq!(
|
|
market_state.highest_coupon_live_bond_principal,
|
|
Some(650_000)
|
|
);
|
|
assert_eq!(market_state.mutable_support_scalar_raw_u32, 0x3f800000);
|
|
assert_eq!(
|
|
market_state.young_company_support_scalar_raw_u32,
|
|
0x42340000
|
|
);
|
|
assert_eq!(market_state.support_progress_word, 17);
|
|
assert_eq!(market_state.recent_per_share_subscore_raw_u32, 0x41f00000);
|
|
assert_eq!(market_state.cached_share_price_raw_u32, 0x426c0000);
|
|
assert_eq!(market_state.chairman_salary_baseline, 24);
|
|
assert_eq!(market_state.chairman_salary_current, 31);
|
|
assert_eq!(market_state.chairman_bonus_year, 1849);
|
|
assert_eq!(market_state.chairman_bonus_amount, 1250);
|
|
assert_eq!(market_state.founding_year, 1842);
|
|
assert_eq!(market_state.last_bankruptcy_year, 1851);
|
|
assert_eq!(market_state.last_dividend_year, 1848);
|
|
assert_eq!(market_state.current_issue_calendar_word, 7);
|
|
assert_eq!(market_state.current_issue_calendar_word_2, 8);
|
|
assert_eq!(market_state.prior_issue_calendar_word, 6);
|
|
assert_eq!(market_state.prior_issue_calendar_word_2, 7);
|
|
assert_eq!(
|
|
roster.entries[0].preferred_locomotive_engine_type_raw_u8,
|
|
Some(2)
|
|
);
|
|
assert!(market_state.city_connection_latch);
|
|
assert!(!market_state.linked_transit_latch);
|
|
assert_eq!(
|
|
market_state.stat_band_root_0cfb_candidates.len(),
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS
|
|
);
|
|
assert_eq!(
|
|
market_state.stat_band_root_0d7f_candidates.len(),
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS
|
|
);
|
|
assert_eq!(
|
|
market_state.stat_band_root_1c47_candidates.len(),
|
|
SAVE_COMPANY_RECORD_STAT_BAND_ROOT_WINDOW_LEN_DWORDS
|
|
);
|
|
assert_eq!(
|
|
market_state.stat_band_root_0cfb_candidates[31].label,
|
|
"stat_band_0cfb_word_32"
|
|
);
|
|
assert_eq!(
|
|
market_state.stat_band_root_0cfb_candidates[31].relative_offset_hex,
|
|
"0xd77"
|
|
);
|
|
assert_eq!(roster.entries[1].company_id, 2);
|
|
assert_eq!(roster.entries[1].current_cash, -25_000);
|
|
assert_eq!(roster.entries[1].linked_chairman_profile_id, Some(2));
|
|
assert_eq!(roster.entries[1].debt, 500_000);
|
|
assert_eq!(roster.entries[1].available_track_laying_capacity, None);
|
|
assert_eq!(roster.entries[1].takeover_cooldown_year, Some(1871));
|
|
let second_market_state = roster.entries[1]
|
|
.market_state
|
|
.as_ref()
|
|
.expect("second company market state should load");
|
|
assert_eq!(
|
|
second_market_state.largest_live_bond_principal,
|
|
Some(500_000)
|
|
);
|
|
assert_eq!(
|
|
second_market_state.highest_coupon_live_bond_principal,
|
|
Some(500_000)
|
|
);
|
|
assert_eq!(second_market_state.chairman_bonus_year, 0);
|
|
assert_eq!(second_market_state.chairman_bonus_amount, 0);
|
|
assert_eq!(second_market_state.last_dividend_year, 1850);
|
|
assert_eq!(second_market_state.current_issue_calendar_word, 3);
|
|
assert_eq!(second_market_state.current_issue_calendar_word_2, 4);
|
|
assert_eq!(second_market_state.prior_issue_calendar_word, 2);
|
|
assert_eq!(second_market_state.prior_issue_calendar_word_2, 3);
|
|
assert_eq!(
|
|
roster.entries[1].preferred_locomotive_engine_type_raw_u8,
|
|
Some(1)
|
|
);
|
|
assert!(!second_market_state.city_connection_latch);
|
|
assert!(second_market_state.linked_transit_latch);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_save_chairman_profile_table_probe_from_direct_records() {
|
|
let metadata_tag_offset = 0x40usize;
|
|
let stride = 0xcabusize;
|
|
let count = 2usize;
|
|
let start_offset = 0x4eusize;
|
|
let total_len = metadata_tag_offset + 4 + start_offset + stride * count + 0x200;
|
|
let mut bytes = vec![0u8; total_len];
|
|
bytes[metadata_tag_offset..metadata_tag_offset + 4]
|
|
.copy_from_slice(&0x00005209u32.to_le_bytes());
|
|
let header_words = [
|
|
1u32,
|
|
0xcab,
|
|
8,
|
|
6,
|
|
8,
|
|
count as u32,
|
|
1,
|
|
1,
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
1,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
];
|
|
for (index, word) in header_words.into_iter().enumerate() {
|
|
let offset = metadata_tag_offset + 4 + index * 4;
|
|
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
|
|
}
|
|
let records_tag_offset = metadata_tag_offset + 4 + 0x100;
|
|
let close_tag_offset = records_tag_offset + 4;
|
|
bytes[records_tag_offset..records_tag_offset + 4]
|
|
.copy_from_slice(&0x0000520au32.to_le_bytes());
|
|
bytes[close_tag_offset..close_tag_offset + 4].copy_from_slice(&0x0000520bu32.to_le_bytes());
|
|
|
|
for (index, (name, linked, cash, cache0, cache1, cache4, holdings)) in [
|
|
(
|
|
"Collis Huntington",
|
|
1u32,
|
|
-107644.0f64,
|
|
252508.0f64,
|
|
0.0f64,
|
|
0.0f64,
|
|
vec![(1u32, 6000u32)],
|
|
),
|
|
(
|
|
"Thomas Durant",
|
|
2u32,
|
|
-382718.0f64,
|
|
-283562.0f64,
|
|
822000.0f64,
|
|
1_392_000.0f64,
|
|
vec![(2u32, 9000u32)],
|
|
),
|
|
]
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
let record_offset = metadata_tag_offset + 4 + start_offset + index * stride;
|
|
bytes[record_offset..record_offset + 4]
|
|
.copy_from_slice(&((index + 1) as u32).to_le_bytes());
|
|
bytes[record_offset + 4..record_offset + 8].copy_from_slice(&1u32.to_le_bytes());
|
|
bytes[record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET
|
|
..record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET + name.len()]
|
|
.copy_from_slice(name.as_bytes());
|
|
bytes[record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET
|
|
..record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET + 8]
|
|
.copy_from_slice(&cash.to_le_bytes());
|
|
bytes[record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET
|
|
..record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET + 4]
|
|
.copy_from_slice(&linked.to_le_bytes());
|
|
bytes[record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET] =
|
|
(index as u8) + 10;
|
|
bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET
|
|
..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET + 8]
|
|
.copy_from_slice(&cache0.to_le_bytes());
|
|
bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET
|
|
..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET + 8]
|
|
.copy_from_slice(&cache1.to_le_bytes());
|
|
bytes[record_offset + 0x211..record_offset + 0x211 + 8]
|
|
.copy_from_slice(&cache4.to_le_bytes());
|
|
for (company_id, units) in holdings {
|
|
let slot_offset = record_offset
|
|
+ SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET
|
|
+ company_id as usize * 4;
|
|
bytes[slot_offset..slot_offset + 4].copy_from_slice(&units.to_le_bytes());
|
|
}
|
|
}
|
|
|
|
let header_probe = parse_save_chairman_profile_collection_header_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
)
|
|
.expect("chairman header probe should parse");
|
|
let table = parse_save_chairman_profile_table_probe(
|
|
&bytes,
|
|
Some(&header_probe),
|
|
Some(&SmpSaveWorldSelectionContextProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-selected-company-and-chairman-context".to_string(),
|
|
chunk_tag_offset: 0,
|
|
payload_offset: 0,
|
|
payload_len: 0,
|
|
payload_len_hex: "0x0".to_string(),
|
|
selected_company_id_offset: 0,
|
|
selected_company_id: 2,
|
|
selected_company_id_hex: "0x00000002".to_string(),
|
|
selected_chairman_profile_id_offset: 0,
|
|
selected_chairman_profile_id: 2,
|
|
selected_chairman_profile_id_hex: "0x00000002".to_string(),
|
|
chairman_slot_selector_offset: 0,
|
|
chairman_slot_selectors: vec![],
|
|
campaign_override_flag_offset: 0,
|
|
campaign_override_flag: 0,
|
|
campaign_override_flag_hex: "0x00".to_string(),
|
|
chairman_role_gate_offset: 0,
|
|
chairman_role_gate_bytes: vec![],
|
|
evidence: vec![],
|
|
}),
|
|
Some(&SmpSaveTaggedCollectionHeaderProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-company-tagged-header-counts".to_string(),
|
|
semantic_family: "scenario-save-company-header-counts".to_string(),
|
|
metadata_tag_offset: 0,
|
|
records_tag_offset: 0,
|
|
close_tag_offset: 0,
|
|
direct_collection_flag: 1,
|
|
direct_collection_flag_hex: "0x00000001".to_string(),
|
|
direct_record_stride: 0x7684,
|
|
direct_record_stride_hex: "0x00007684".to_string(),
|
|
live_id_bound: 5,
|
|
live_id_bound_hex: "0x00000005".to_string(),
|
|
live_record_count: 2,
|
|
live_record_count_hex: "0x00000002".to_string(),
|
|
header_words: vec![],
|
|
header_hex_words: vec![],
|
|
evidence: vec![],
|
|
}),
|
|
)
|
|
.expect("chairman profile table should parse");
|
|
|
|
assert_eq!(table.observed_entry_count, 2);
|
|
assert_eq!(table.selected_chairman_profile_id, Some(2));
|
|
assert_eq!(table.entries.len(), 2);
|
|
assert_eq!(table.entries[0].profile_id, 1);
|
|
assert_eq!(table.entries[0].name, "Collis Huntington");
|
|
assert_eq!(table.entries[0].linked_company_id, Some(1));
|
|
assert_eq!(table.entries[0].company_holdings.get(&1), Some(&6000));
|
|
assert_eq!(table.entries[0].current_cash, -107644);
|
|
assert_eq!(table.entries[0].holdings_value_total, 252508);
|
|
assert_eq!(table.entries[0].purchasing_power_total, 144864);
|
|
assert_eq!(table.entries[0].personality_byte_0x291, Some(10));
|
|
assert_eq!(table.entries[1].profile_id, 2);
|
|
assert_eq!(table.entries[1].company_holdings.get(&2), Some(&9000));
|
|
assert_eq!(table.entries[1].holdings_value_total, 822000);
|
|
assert_eq!(table.entries[1].purchasing_power_total, 1_009_282);
|
|
assert_eq!(table.entries[1].personality_byte_0x291, Some(11));
|
|
}
|
|
|
|
#[test]
|
|
fn builds_save_world_selection_role_analysis_from_probe() {
|
|
let probe = SmpSaveWorldSelectionContextProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
source_kind: "save-direct-world-block".to_string(),
|
|
semantic_family: "scenario-selected-company-and-chairman-context".to_string(),
|
|
chunk_tag_offset: 0,
|
|
payload_offset: 0,
|
|
payload_len: 0x4f2c,
|
|
payload_len_hex: "0x4f2c".to_string(),
|
|
selected_company_id_offset: 0x21,
|
|
selected_company_id: 3,
|
|
selected_company_id_hex: "0x00000003".to_string(),
|
|
selected_chairman_profile_id_offset: 0x25,
|
|
selected_chairman_profile_id: 7,
|
|
selected_chairman_profile_id_hex: "0x00000007".to_string(),
|
|
chairman_slot_selector_offset: 0x87,
|
|
chairman_slot_selectors: vec![1, 0, 2, 0],
|
|
campaign_override_flag_offset: 0xc5,
|
|
campaign_override_flag: 1,
|
|
campaign_override_flag_hex: "0x01".to_string(),
|
|
chairman_role_gate_offset: 0x0bc3,
|
|
chairman_role_gate_bytes: vec![2, 0, 1, 0],
|
|
evidence: vec![],
|
|
};
|
|
|
|
let analysis = build_save_world_selection_role_analysis(&probe);
|
|
|
|
assert_eq!(analysis.selected_company_id, 3);
|
|
assert_eq!(analysis.selected_chairman_profile_id, 7);
|
|
assert_eq!(analysis.campaign_override_flag_hex, "0x01");
|
|
assert_eq!(analysis.chairman_slots.len(), 4);
|
|
assert_eq!(analysis.chairman_slots[0].selector_byte_hex, "0x01");
|
|
assert_eq!(analysis.chairman_slots[2].role_gate_byte_hex, "0x01");
|
|
}
|
|
|
|
#[test]
|
|
fn builds_save_candidate_views_with_raw_bits() {
|
|
let mut bytes = vec![0u8; 0x40];
|
|
bytes[0x08..0x0c].copy_from_slice(&0x3f800000u32.to_le_bytes());
|
|
bytes[0x10..0x18].copy_from_slice(&(-2458.0f64).to_le_bytes());
|
|
|
|
let dword = build_save_dword_candidate(&bytes, 0, "unit_float", 0x08)
|
|
.expect("dword candidate should build");
|
|
let qword =
|
|
build_save_qword_candidate(&bytes, 0, 0x10).expect("qword candidate should build");
|
|
|
|
assert_eq!(dword.raw_u32_hex, "0x3f800000");
|
|
assert_eq!(dword.value_i32, 1_065_353_216);
|
|
assert_eq!(dword.value_f32, 1.0);
|
|
assert_eq!(qword.raw_u64, (-2458.0f64).to_bits());
|
|
assert_eq!(qword.value_i64, (-2458.0f64).to_bits() as i64);
|
|
assert_eq!(qword.value_f64, -2458.0);
|
|
}
|
|
|
|
#[test]
|
|
fn derives_chairman_holdings_share_price_total_from_grounded_company_prices() {
|
|
let holdings_by_company =
|
|
BTreeMap::from([(2u32, 19_000u32), (4u32, 1_000u32), (6u32, 2_000u32)]);
|
|
let company_share_prices = BTreeMap::from([(2u32, 66i64), (4u32, 69i64), (6u32, 27i64)]);
|
|
|
|
let total =
|
|
derive_chairman_holdings_share_price_total(&holdings_by_company, &company_share_prices)
|
|
.expect("derived holdings total should compute");
|
|
|
|
assert_eq!(total, 1_377_000);
|
|
}
|
|
|
|
#[test]
|
|
fn derives_chairman_cached_purchasing_power_from_strongest_nonnegative_cache() {
|
|
let cached_scalar_candidates = vec![
|
|
SmpSaveScalarCandidate {
|
|
relative_offset: 0x1e9,
|
|
relative_offset_hex: "0x1e9".to_string(),
|
|
raw_u64: (-343_508.0f64).to_bits(),
|
|
raw_u64_hex: format!("0x{:016x}", (-343_508.0f64).to_bits()),
|
|
value_i64: round_f64_to_i64(-343_508.0).expect("i64"),
|
|
value_f64: -343_508.0,
|
|
},
|
|
SmpSaveScalarCandidate {
|
|
relative_offset: 0x201,
|
|
relative_offset_hex: "0x201".to_string(),
|
|
raw_u64: 1_386_000.0f64.to_bits(),
|
|
raw_u64_hex: format!("0x{:016x}", 1_386_000.0f64.to_bits()),
|
|
value_i64: round_f64_to_i64(1_386_000.0).expect("i64"),
|
|
value_f64: 1_386_000.0,
|
|
},
|
|
SmpSaveScalarCandidate {
|
|
relative_offset: 0x211,
|
|
relative_offset_hex: "0x211".to_string(),
|
|
raw_u64: 1_392_000.0f64.to_bits(),
|
|
raw_u64_hex: format!("0x{:016x}", 1_392_000.0f64.to_bits()),
|
|
value_i64: round_f64_to_i64(1_392_000.0).expect("i64"),
|
|
value_f64: 1_392_000.0,
|
|
},
|
|
];
|
|
|
|
let total =
|
|
derive_chairman_cached_purchasing_power_total(-463_436, &cached_scalar_candidates)
|
|
.expect("derived purchasing power should compute");
|
|
|
|
assert_eq!(total, 928_564);
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_rt3_105_post_span_bridge_variants() {
|
|
let base_trailer = SmpRuntimeTrailerBlock {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
trailer_family: "rt3-105-save-trailer-v1".to_string(),
|
|
trailer_evidence: vec![],
|
|
trailer_offset: 944,
|
|
prefix_words_0_to_5: vec![],
|
|
prefix_hex_words_0_to_5: vec![],
|
|
tag_word_6: 0,
|
|
tag_word_6_hex: String::new(),
|
|
tag_chunk_id_u16: 0x2ee1,
|
|
tag_chunk_id_hex: "0x2ee1".to_string(),
|
|
tag_chunk_id_grounded_alignment: None,
|
|
length_word_7: 0x32c8_0000,
|
|
length_word_7_hex: "0x32c80000".to_string(),
|
|
length_high_u16: 0x32c8,
|
|
length_high_hex: "0x32c8".to_string(),
|
|
selector_word_8: 0x7110_0000,
|
|
selector_word_8_hex: "0x71100000".to_string(),
|
|
selector_high_u16: 0x7110,
|
|
selector_high_hex: "0x7110".to_string(),
|
|
layout_word_9: 0,
|
|
layout_word_9_hex: String::new(),
|
|
descriptor_word_10: 0x7801_0000,
|
|
descriptor_word_10_hex: "0x78010000".to_string(),
|
|
descriptor_high_u16: 0x7801,
|
|
descriptor_high_hex: "0x7801".to_string(),
|
|
descriptor_word_11: 0,
|
|
descriptor_word_11_hex: String::new(),
|
|
counter_word_12: 0,
|
|
counter_word_12_hex: String::new(),
|
|
offset_word_13: 0,
|
|
offset_word_13_hex: String::new(),
|
|
span_word_14: 0,
|
|
span_word_14_hex: String::new(),
|
|
mode_word_15: 0,
|
|
mode_word_15_hex: String::new(),
|
|
words: vec![],
|
|
hex_words: vec![],
|
|
};
|
|
let base_post_span = SmpRuntimePostSpanProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
span_target_offset: 13944,
|
|
next_nonzero_offset: Some(14795),
|
|
next_aligned_candidate_offset: Some(20244),
|
|
next_aligned_candidate_words: vec![],
|
|
next_aligned_candidate_hex_words: vec![],
|
|
header_candidates: vec![SmpRuntimePostSpanHeaderCandidate {
|
|
offset: 20244,
|
|
words: vec![],
|
|
hex_words: vec![],
|
|
dense_word_count: 3,
|
|
high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515],
|
|
high_hex_words: vec![],
|
|
grounded_alignments: vec![],
|
|
}],
|
|
grounded_progress_hits: vec![],
|
|
};
|
|
let base_profile = SmpRt3105PackedProfileProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
packed_profile_offset: 29632,
|
|
packed_profile_len: 0x108,
|
|
packed_profile_len_hex: "0x108".to_string(),
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
|
relative_len: 0x108,
|
|
relative_len_hex: "0x108".to_string(),
|
|
leading_word_0: 3,
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
|
trailing_zero_word_count_after_leading_word: 2,
|
|
header_flag_word_3: 0x0100_0000,
|
|
header_flag_word_3_hex: "0x01000000".to_string(),
|
|
map_path_offset: 0x10,
|
|
map_path: Some("Alternate USA.gmp".to_string()),
|
|
display_name_offset: 0x43,
|
|
display_name: Some("Alternate USA".to_string()),
|
|
profile_byte_0x77: 0x07,
|
|
profile_byte_0x77_hex: "0x07".to_string(),
|
|
profile_byte_0x82: 0x4d,
|
|
profile_byte_0x82_hex: "0x4d".to_string(),
|
|
profile_byte_0x97: 0,
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
|
profile_byte_0xc5: 0,
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
|
stable_nonzero_words: vec![],
|
|
},
|
|
ascii_runs: vec![],
|
|
};
|
|
let base_bridge = parse_rt3_105_post_span_bridge_probe(
|
|
Some(&base_trailer),
|
|
Some(&base_post_span),
|
|
Some(&base_profile),
|
|
)
|
|
.expect("base bridge should parse");
|
|
assert_eq!(
|
|
base_bridge.bridge_family,
|
|
"rt3-105-save-post-span-bridge-v1"
|
|
);
|
|
assert_eq!(base_bridge.packed_profile_delta_from_span_target, 15688);
|
|
assert_eq!(
|
|
base_bridge.next_candidate_delta_from_packed_profile,
|
|
Some(-9388)
|
|
);
|
|
let base_variant_trailer = SmpRuntimeTrailerBlock {
|
|
descriptor_word_10: 0x7401_0000,
|
|
descriptor_word_10_hex: "0x74010000".to_string(),
|
|
descriptor_high_u16: 0x7401,
|
|
descriptor_high_hex: "0x7401".to_string(),
|
|
..base_trailer.clone()
|
|
};
|
|
let base_variant_bridge = parse_rt3_105_post_span_bridge_probe(
|
|
Some(&base_variant_trailer),
|
|
Some(&base_post_span),
|
|
Some(&base_profile),
|
|
)
|
|
.expect("base bridge variant should parse");
|
|
assert_eq!(
|
|
base_variant_bridge.bridge_family,
|
|
"rt3-105-save-post-span-bridge-v1"
|
|
);
|
|
|
|
let alt_trailer = SmpRuntimeTrailerBlock {
|
|
profile_family: "rt3-105-alt-save-container-v1".to_string(),
|
|
selector_word_8: 0x54cd_0000,
|
|
selector_word_8_hex: "0x54cd0000".to_string(),
|
|
selector_high_u16: 0x54cd,
|
|
selector_high_hex: "0x54cd".to_string(),
|
|
descriptor_word_10: 0x5901_0000,
|
|
descriptor_word_10_hex: "0x59010000".to_string(),
|
|
descriptor_high_u16: 0x5901,
|
|
descriptor_high_hex: "0x5901".to_string(),
|
|
..base_trailer.clone()
|
|
};
|
|
let alt_post_span = SmpRuntimePostSpanProbe {
|
|
profile_family: "rt3-105-alt-save-container-v1".to_string(),
|
|
next_aligned_candidate_offset: Some(29892),
|
|
header_candidates: vec![SmpRuntimePostSpanHeaderCandidate {
|
|
offset: 29892,
|
|
words: vec![],
|
|
hex_words: vec![],
|
|
dense_word_count: 3,
|
|
high_u16_words: vec![0x1500, 0x0100, 0x4100, 0x0200],
|
|
high_hex_words: vec![],
|
|
grounded_alignments: vec![],
|
|
}],
|
|
..base_post_span.clone()
|
|
};
|
|
let alt_profile = SmpRt3105PackedProfileProbe {
|
|
profile_family: "rt3-105-alt-save-container-v1".to_string(),
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
|
map_path: Some("Spanish Mainline.gmp".to_string()),
|
|
display_name: Some("Spanish Mainline".to_string()),
|
|
profile_byte_0x82: 0xa3,
|
|
profile_byte_0x82_hex: "0xa3".to_string(),
|
|
..base_profile.packed_profile_block.clone()
|
|
},
|
|
..base_profile.clone()
|
|
};
|
|
let alt_bridge = parse_rt3_105_post_span_bridge_probe(
|
|
Some(&alt_trailer),
|
|
Some(&alt_post_span),
|
|
Some(&alt_profile),
|
|
)
|
|
.expect("alt bridge should parse");
|
|
assert_eq!(
|
|
alt_bridge.bridge_family,
|
|
"rt3-105-alt-save-post-span-bridge-v1"
|
|
);
|
|
assert_eq!(
|
|
alt_bridge.next_candidate_delta_from_packed_profile,
|
|
Some(260)
|
|
);
|
|
|
|
let scenario_trailer = SmpRuntimeTrailerBlock {
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
|
trailer_family: "unknown".to_string(),
|
|
trailer_offset: 864,
|
|
length_word_7: 0,
|
|
length_word_7_hex: "0x00000000".to_string(),
|
|
length_high_u16: 0,
|
|
length_high_hex: "0x0000".to_string(),
|
|
selector_word_8: 0x0001_86a0,
|
|
selector_word_8_hex: "0x000186a0".to_string(),
|
|
selector_high_u16: 0x0001,
|
|
selector_high_hex: "0x0001".to_string(),
|
|
descriptor_word_10: 0x0186_a000,
|
|
descriptor_word_10_hex: "0x0186a000".to_string(),
|
|
descriptor_high_u16: 0x0186,
|
|
descriptor_high_hex: "0x0186".to_string(),
|
|
..base_trailer.clone()
|
|
};
|
|
let scenario_post_span = SmpRuntimePostSpanProbe {
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
|
span_target_offset: 864,
|
|
next_aligned_candidate_offset: Some(940),
|
|
header_candidates: vec![SmpRuntimePostSpanHeaderCandidate {
|
|
offset: 940,
|
|
words: vec![],
|
|
hex_words: vec![],
|
|
dense_word_count: 3,
|
|
high_u16_words: vec![0x0186, 0x0006, 0x0006, 0x0001],
|
|
high_hex_words: vec![],
|
|
grounded_alignments: vec![],
|
|
}],
|
|
..base_post_span.clone()
|
|
};
|
|
let scenario_profile = SmpRt3105PackedProfileProbe {
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
|
map_path: Some("Southern Pacific.gmp".to_string()),
|
|
display_name: Some("Southern Pacific".to_string()),
|
|
profile_byte_0x82: 0x90,
|
|
profile_byte_0x82_hex: "0x90".to_string(),
|
|
..base_profile.packed_profile_block.clone()
|
|
},
|
|
..base_profile.clone()
|
|
};
|
|
let scenario_bridge = parse_rt3_105_post_span_bridge_probe(
|
|
Some(&scenario_trailer),
|
|
Some(&scenario_post_span),
|
|
Some(&scenario_profile),
|
|
)
|
|
.expect("scenario bridge should parse");
|
|
assert_eq!(
|
|
scenario_bridge.bridge_family,
|
|
"rt3-105-scenario-post-span-bridge-v1"
|
|
);
|
|
assert_eq!(
|
|
scenario_bridge.next_candidate_delta_from_packed_profile,
|
|
Some(-28692)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_rt3_105_save_bridge_payload_probe() {
|
|
let mut bytes = vec![0u8; 0x7000];
|
|
let primary = 0x4f14usize;
|
|
let secondary = 0x671cusize;
|
|
let primary_words: [u32; 8] = [
|
|
0x62000000, 0x00000000, 0xfff70000, 0x55150000, 0x55550000, 0x00000000, 0xfff70000,
|
|
0x54550000,
|
|
];
|
|
for (index, word) in primary_words.iter().enumerate() {
|
|
bytes[primary + index * 4..primary + (index + 1) * 4]
|
|
.copy_from_slice(&(*word).to_le_bytes());
|
|
}
|
|
|
|
let secondary_words: [u32; 8] = [
|
|
0x00050000, 0x00050005, 0xfff70000, 0x54540000, 0x545400f9, 0x00f900f9, 0x00f94008,
|
|
0x00001555,
|
|
];
|
|
for (index, word) in secondary_words.iter().enumerate() {
|
|
bytes[secondary + index * 4..secondary + (index + 1) * 4]
|
|
.copy_from_slice(&(*word).to_le_bytes());
|
|
}
|
|
|
|
let bridge_probe = SmpRt3105PostSpanBridgeProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
|
bridge_evidence: vec![],
|
|
span_target_offset: 0x3678,
|
|
next_candidate_offset: Some(primary),
|
|
next_candidate_delta_from_span_target: Some(primary - 0x3678),
|
|
packed_profile_offset: 0x73c0,
|
|
packed_profile_delta_from_span_target: 0x3d48,
|
|
next_candidate_delta_from_packed_profile: Some(primary as i64 - 0x73c0),
|
|
selector_high_u16: 0x7110,
|
|
selector_high_hex: "0x7110".to_string(),
|
|
descriptor_high_u16: 0x7801,
|
|
descriptor_high_hex: "0x7801".to_string(),
|
|
next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515],
|
|
next_candidate_high_hex_words: vec![],
|
|
};
|
|
|
|
let probe = parse_rt3_105_save_bridge_payload_probe(&bytes, Some(&bridge_probe))
|
|
.expect("save bridge payload probe should parse");
|
|
|
|
assert_eq!(probe.primary_block_offset, primary);
|
|
assert_eq!(probe.primary_block_len, 0x20);
|
|
assert_eq!(probe.secondary_block_offset, secondary);
|
|
assert_eq!(probe.secondary_block_delta_from_primary, 0x1808);
|
|
assert_eq!(probe.secondary_block_end_offset, 0x73c0);
|
|
assert_eq!(probe.secondary_block_len, 0xca4);
|
|
assert_eq!(probe.primary_words[..4], primary_words[..4]);
|
|
assert_eq!(probe.secondary_words[..8], secondary_words[..8]);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_rt3_105_save_name_table_probe() {
|
|
let mut bytes = vec![0u8; 0x7400];
|
|
let secondary = 0x671cusize;
|
|
let header = secondary + 0x354;
|
|
let entries = secondary + 0x3b5;
|
|
let stride = 0x22usize;
|
|
let names = ["AluminumMill", "Nuclear Power Plant", "Bakery"];
|
|
|
|
bytes[header..header + 4].copy_from_slice(&0x10000000u32.to_le_bytes());
|
|
bytes[header + 4..header + 8].copy_from_slice(&0x00009000u32.to_le_bytes());
|
|
bytes[header + 8..header + 12].copy_from_slice(&0x0000332eu32.to_le_bytes());
|
|
bytes[header + 0x1c..header + 0x20].copy_from_slice(&4u32.to_le_bytes());
|
|
bytes[header + 0x20..header + 0x24].copy_from_slice(&(names.len() as u32).to_le_bytes());
|
|
bytes[header + 12..header + 16].copy_from_slice(&1u32.to_le_bytes());
|
|
bytes[header + 16..header + 20].copy_from_slice(&0x22u32.to_le_bytes());
|
|
bytes[header + 20..header + 24].copy_from_slice(&2u32.to_le_bytes());
|
|
bytes[header + 24..header + 28].copy_from_slice(&2u32.to_le_bytes());
|
|
bytes[header + 0x28..header + 0x2c].copy_from_slice(&1u32.to_le_bytes());
|
|
|
|
for (index, name) in names.iter().enumerate() {
|
|
let off = entries + index * stride;
|
|
let raw = &mut bytes[off..off + stride];
|
|
raw[..name.len()].copy_from_slice(name.as_bytes());
|
|
let trailer = if *name == "Nuclear Power Plant" {
|
|
0u32
|
|
} else {
|
|
1u32
|
|
};
|
|
raw[stride - 4..stride].copy_from_slice(&trailer.to_le_bytes());
|
|
}
|
|
let footer = entries + names.len() * stride;
|
|
bytes[footer..footer + 4].copy_from_slice(&0x32dcu32.to_le_bytes());
|
|
bytes[footer + 4..footer + 8].copy_from_slice(&0x3714u32.to_le_bytes());
|
|
bytes[footer + 8] = 0x00;
|
|
|
|
let payload = SmpRt3105SaveBridgePayloadProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
|
primary_block_offset: 0x4f14,
|
|
primary_block_len: 0x20,
|
|
primary_block_len_hex: "0x20".to_string(),
|
|
primary_words: vec![],
|
|
primary_hex_words: vec![],
|
|
secondary_block_offset: secondary,
|
|
secondary_block_delta_from_primary: 0x1808,
|
|
secondary_block_delta_from_primary_hex: "0x1808".to_string(),
|
|
secondary_block_end_offset: footer + 9,
|
|
secondary_block_len: footer + 9 - secondary,
|
|
secondary_block_len_hex: format!("0x{:x}", footer + 9 - secondary),
|
|
secondary_preview_word_count: 32,
|
|
secondary_words: vec![],
|
|
secondary_hex_words: vec![],
|
|
evidence: vec![],
|
|
};
|
|
|
|
let probe = parse_rt3_105_save_name_table_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&payload),
|
|
)
|
|
.expect("save name table probe should parse");
|
|
|
|
assert_eq!(probe.source_kind, "save-bridge-secondary-block");
|
|
assert_eq!(
|
|
probe.semantic_family,
|
|
"scenario-named-candidate-availability-table"
|
|
);
|
|
assert_eq!(probe.header_offset, header);
|
|
assert_eq!(probe.entry_stride, stride);
|
|
assert_eq!(probe.observed_entry_capacity, 4);
|
|
assert_eq!(probe.observed_entry_count, names.len());
|
|
assert_eq!(probe.entries[0].text, "AluminumMill");
|
|
assert_eq!(probe.entries[0].availability_dword, 1);
|
|
assert_eq!(probe.entries[2].text, "Bakery");
|
|
assert_eq!(probe.zero_trailer_entry_count, 1);
|
|
assert_eq!(
|
|
probe.zero_trailer_entry_names,
|
|
vec!["Nuclear Power Plant".to_string()]
|
|
);
|
|
assert_eq!(probe.trailing_footer_hex, "dc3200001437000000");
|
|
assert_eq!(probe.footer_progress_word_0, 0x32dc);
|
|
assert_eq!(probe.footer_progress_word_1, 0x3714);
|
|
assert_eq!(probe.footer_trailing_byte, 0x00);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_rt3_105_map_name_table_probe_from_fixed_offsets() {
|
|
let mut bytes = vec![0u8; 0x7400];
|
|
let header = 0x6a70usize;
|
|
let entries = 0x6ad1usize;
|
|
let stride = 0x22usize;
|
|
let observed_entry_count = 67usize;
|
|
|
|
bytes[header..header + 4].copy_from_slice(&0x00000000u32.to_le_bytes());
|
|
bytes[header + 4..header + 8].copy_from_slice(&0x00000000u32.to_le_bytes());
|
|
bytes[header + 8..header + 12].copy_from_slice(&0x0000332eu32.to_le_bytes());
|
|
bytes[header + 12..header + 16].copy_from_slice(&1u32.to_le_bytes());
|
|
bytes[header + 16..header + 20].copy_from_slice(&0x22u32.to_le_bytes());
|
|
bytes[header + 20..header + 24].copy_from_slice(&2u32.to_le_bytes());
|
|
bytes[header + 24..header + 28].copy_from_slice(&2u32.to_le_bytes());
|
|
bytes[header + 0x1c..header + 0x20].copy_from_slice(&0x44u32.to_le_bytes());
|
|
bytes[header + 0x20..header + 0x24]
|
|
.copy_from_slice(&(observed_entry_count as u32).to_le_bytes());
|
|
bytes[header + 0x28..header + 0x2c].copy_from_slice(&1u32.to_le_bytes());
|
|
|
|
for index in 0..observed_entry_count {
|
|
let name = match index {
|
|
0 => "AutoPlant".to_string(),
|
|
1 => "Nuclear Power Plant".to_string(),
|
|
66 => "Warehouse11".to_string(),
|
|
_ => format!("Entry{index:02}"),
|
|
};
|
|
let off = entries + index * stride;
|
|
let raw = &mut bytes[off..off + stride];
|
|
raw[..name.len()].copy_from_slice(name.as_bytes());
|
|
let trailer = if name == "Nuclear Power Plant" {
|
|
0u32
|
|
} else {
|
|
1u32
|
|
};
|
|
raw[stride - 4..stride].copy_from_slice(&trailer.to_le_bytes());
|
|
}
|
|
|
|
let footer = entries + observed_entry_count * stride;
|
|
bytes[footer..footer + 4].copy_from_slice(&0x32dcu32.to_le_bytes());
|
|
bytes[footer + 4..footer + 8].copy_from_slice(&0x3714u32.to_le_bytes());
|
|
bytes[footer + 8] = 0x00;
|
|
|
|
let probe = parse_rt3_105_save_name_table_probe(
|
|
&bytes,
|
|
Some("gmp"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-map-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
None,
|
|
)
|
|
.expect("map name table probe should parse");
|
|
|
|
assert_eq!(probe.profile_family, "rt3-105-map-container-v1");
|
|
assert_eq!(probe.source_kind, "map-fixed-catalog-range");
|
|
assert_eq!(probe.header_offset, header);
|
|
assert_eq!(probe.entries_offset, entries);
|
|
assert_eq!(probe.observed_entry_count, observed_entry_count);
|
|
assert_eq!(probe.entries[0].text, "AutoPlant");
|
|
assert_eq!(probe.entries[66].text, "Warehouse11");
|
|
assert_eq!(
|
|
probe.zero_trailer_entry_names,
|
|
vec!["Nuclear Power Plant".to_string()]
|
|
);
|
|
assert_eq!(probe.footer_progress_word_0, 0x32dc);
|
|
assert_eq!(probe.footer_progress_word_1, 0x3714);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_rt3_105_save_named_locomotive_availability_probe() {
|
|
let mut bytes = vec![0u8; 0x9000];
|
|
let packed_profile_offset = 0x73c0usize;
|
|
let packed_profile_len = 0x108usize;
|
|
let entries_offset = 0x7c78usize;
|
|
let names = [
|
|
("Eight Wheeler 4-4-0", 1u32),
|
|
("EP-2 Bipolar", 1u32),
|
|
("ET22", 1u32),
|
|
("F3", 0u32),
|
|
("Fairlie 0-6-6-0", 1u32),
|
|
("Firefly 2-2-2", 0u32),
|
|
("FP45", 0u32),
|
|
("Ge 6/6 Crocodile", 1u32),
|
|
("GG1", 0u32),
|
|
("GP7", 1u32),
|
|
];
|
|
|
|
for (index, (name, value)) in names.iter().enumerate() {
|
|
let offset = entries_offset + index * RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE;
|
|
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
|
bytes[offset + 4..offset + 4 + name.len()].copy_from_slice(name.as_bytes());
|
|
}
|
|
|
|
let probe = parse_rt3_105_save_named_locomotive_availability_probe(
|
|
&bytes,
|
|
Some("gms"),
|
|
Some(&SmpContainerProfile {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
profile_evidence: vec![],
|
|
is_known_profile: true,
|
|
}),
|
|
Some(&SmpRt3105PackedProfileProbe {
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
packed_profile_offset,
|
|
packed_profile_len,
|
|
packed_profile_len_hex: "0x108".to_string(),
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
|
relative_len: packed_profile_len,
|
|
relative_len_hex: "0x108".to_string(),
|
|
leading_word_0: 3,
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
|
trailing_zero_word_count_after_leading_word: 2,
|
|
header_flag_word_3: 1,
|
|
header_flag_word_3_hex: "0x00000001".to_string(),
|
|
map_path_offset: 0x10,
|
|
map_path: Some("Alternate USA.gmp".to_string()),
|
|
display_name_offset: 0x43,
|
|
display_name: Some("Alternate USA".to_string()),
|
|
profile_byte_0x77: 0x07,
|
|
profile_byte_0x77_hex: "0x07".to_string(),
|
|
profile_byte_0x82: 0x4d,
|
|
profile_byte_0x82_hex: "0x4d".to_string(),
|
|
profile_byte_0x97: 0,
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
|
profile_byte_0xc5: 0,
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
|
stable_nonzero_words: vec![],
|
|
},
|
|
ascii_runs: vec![],
|
|
}),
|
|
)
|
|
.expect("save-side locomotive table probe should parse");
|
|
|
|
assert_eq!(probe.source_kind, "save-direct-locomotive-row-run");
|
|
assert_eq!(
|
|
probe.semantic_family,
|
|
"scenario-named-locomotive-availability-table"
|
|
);
|
|
assert_eq!(probe.entries_offset, entries_offset);
|
|
assert_eq!(
|
|
probe.entry_stride,
|
|
RT3_105_SAVE_NAMED_LOCOMOTIVE_ENTRY_STRIDE
|
|
);
|
|
assert_eq!(probe.observed_entry_count, names.len());
|
|
assert_eq!(probe.zero_availability_count, 4);
|
|
assert_eq!(probe.entries[0].text, "Eight Wheeler 4-4-0");
|
|
assert_eq!(probe.entries[9].text, "GP7");
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_rt3_105_alt_save_container_profile() {
|
|
let shared_header = SmpSharedHeader {
|
|
byte_len: 64,
|
|
root_kind_word: 0x000025e5,
|
|
root_kind_word_hex: "0x000025e5".to_string(),
|
|
primary_family_tag: 0x00002ee0,
|
|
primary_family_tag_hex: "0x00002ee0".to_string(),
|
|
shared_signature_words_1_to_7: vec![
|
|
0x00002ee0, 0x0001c001, 0x00018000, 0x00010000, 0x00000754, 0x00000754, 0x00000754,
|
|
],
|
|
shared_signature_hex_words_1_to_7: vec![
|
|
"0x00002ee0".to_string(),
|
|
"0x0001c001".to_string(),
|
|
"0x00018000".to_string(),
|
|
"0x00010000".to_string(),
|
|
"0x00000754".to_string(),
|
|
"0x00000754".to_string(),
|
|
"0x00000754".to_string(),
|
|
],
|
|
matches_grounded_common_signature: false,
|
|
payload_window_words_8_to_9: vec![0x007a5978, 0x007a9022],
|
|
payload_window_hex_words_8_to_9: vec![
|
|
"0x007a5978".to_string(),
|
|
"0x007a9022".to_string(),
|
|
],
|
|
reserved_words_10_to_14: vec![0; 5],
|
|
reserved_words_10_to_14_all_zero: true,
|
|
final_flag_word: 0,
|
|
final_flag_word_hex: "0x00000000".to_string(),
|
|
};
|
|
let early_content_probe = SmpEarlyContentProbe {
|
|
first_post_text_nonzero_offset: 722,
|
|
zero_pad_after_text_len: 431,
|
|
first_post_text_block_len: 35,
|
|
first_post_text_block_hex:
|
|
"0101010000010000000000000100000000000000010000000000000000010100000001".to_string(),
|
|
trailing_zero_pad_after_first_block_len: 45,
|
|
secondary_nonzero_offset: Some(802),
|
|
secondary_aligned_word_window_offset: Some(800),
|
|
secondary_aligned_word_window_words: vec![
|
|
0x00010000, 0x49f00100, 0x00000002, 0xa0000000, 0x00000186, 0x00000000, 0x000186a0,
|
|
0x00000000,
|
|
],
|
|
secondary_aligned_word_window_hex_words: vec![
|
|
"0x00010000".to_string(),
|
|
"0x49f00100".to_string(),
|
|
"0x00000002".to_string(),
|
|
"0xa0000000".to_string(),
|
|
"0x00000186".to_string(),
|
|
"0x00000000".to_string(),
|
|
"0x000186a0".to_string(),
|
|
"0x00000000".to_string(),
|
|
],
|
|
secondary_preview_hex:
|
|
"01000001f04902000000000000a08601000000000000a08601000000000000a0".to_string(),
|
|
};
|
|
|
|
let header_variant = classify_header_variant_probe(&shared_header);
|
|
let secondary_variant =
|
|
classify_secondary_variant_probe(&early_content_probe).expect("secondary probe");
|
|
let container_profile = classify_container_profile(
|
|
Some("gms"),
|
|
Some(&header_variant),
|
|
Some(&secondary_variant),
|
|
)
|
|
.expect("container profile");
|
|
|
|
assert_eq!(header_variant.variant_family, "rt3-105-alt-save-header-v1");
|
|
assert_eq!(
|
|
secondary_variant.variant_family,
|
|
"rt3-105-gms-alt-family-v1"
|
|
);
|
|
assert_eq!(
|
|
container_profile.profile_family,
|
|
"rt3-105-alt-save-container-v1"
|
|
);
|
|
assert!(container_profile.is_known_profile);
|
|
}
|
|
|
|
#[test]
|
|
fn classifies_rt3_105_map_container_profiles_from_header_families() {
|
|
let scenario_profile = classify_container_profile(
|
|
Some("gmp"),
|
|
Some(&SmpHeaderVariantProbe {
|
|
variant_family: "rt3-105-scenario-save-header-v1".to_string(),
|
|
variant_evidence: vec![],
|
|
is_known_family: true,
|
|
}),
|
|
Some(&SmpSecondaryVariantProbe {
|
|
aligned_window_offset: 0,
|
|
words: vec![1, 0, 0, 0],
|
|
hex_words: vec![],
|
|
variant_family: "unknown".to_string(),
|
|
variant_evidence: vec![],
|
|
}),
|
|
)
|
|
.expect("scenario map profile");
|
|
|
|
let alt_profile = classify_container_profile(
|
|
Some("gmp"),
|
|
Some(&SmpHeaderVariantProbe {
|
|
variant_family: "rt3-105-alt-save-header-v1".to_string(),
|
|
variant_evidence: vec![],
|
|
is_known_family: true,
|
|
}),
|
|
Some(&SmpSecondaryVariantProbe {
|
|
aligned_window_offset: 0,
|
|
words: vec![0x49f00100, 2, 0xa0000000, 0x186],
|
|
hex_words: vec![],
|
|
variant_family: "unknown".to_string(),
|
|
variant_evidence: vec![],
|
|
}),
|
|
)
|
|
.expect("alt map profile");
|
|
|
|
assert_eq!(
|
|
scenario_profile.profile_family,
|
|
"rt3-105-scenario-map-container-v1"
|
|
);
|
|
assert!(scenario_profile.is_known_profile);
|
|
assert_eq!(alt_profile.profile_family, "rt3-105-alt-map-container-v1");
|
|
assert!(alt_profile.is_known_profile);
|
|
}
|
|
|
|
fn empty_analysis_report() -> SmpSaveCompanyChairmanAnalysisReport {
|
|
SmpSaveCompanyChairmanAnalysisReport {
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
|
selected_company_id: None,
|
|
selected_chairman_profile_id: None,
|
|
world_selection_context: None,
|
|
world_issue_37: None,
|
|
world_economic_tuning: None,
|
|
world_finance_neighborhood: None,
|
|
train_collection_header: None,
|
|
train_collection_directory: None,
|
|
region_collection_header: None,
|
|
region_record_triplets: None,
|
|
region_queued_notice_records: None,
|
|
placed_structure_collection_header: None,
|
|
placed_structure_record_triplets: None,
|
|
placed_structure_dynamic_side_buffer: None,
|
|
placed_structure_dynamic_side_buffer_alignment: None,
|
|
unclassified_tagged_collection_headers: Vec::new(),
|
|
company_entries: Vec::new(),
|
|
chairman_entries: Vec::new(),
|
|
notes: Vec::new(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn builds_region_service_trace_report_with_explicit_latch_blockers() {
|
|
let mut analysis = empty_analysis_report();
|
|
analysis.region_record_triplets = Some(SmpSaveRegionRecordTripletProbe {
|
|
profile_family: analysis.profile_family.clone(),
|
|
source_kind: "save-region-record-triplets".to_string(),
|
|
semantic_family: "marker09".to_string(),
|
|
records_tag_offset: 0,
|
|
close_tag_offset: 0,
|
|
record_count: 1,
|
|
entries: vec![SmpSaveRegionRecordTripletEntryProbe {
|
|
record_index: 0,
|
|
name: "Marker09".to_string(),
|
|
name_tag_relative_offset: 0,
|
|
policy_tag_relative_offset: 0,
|
|
profile_tag_relative_offset: 0,
|
|
policy_chunk_len: 0,
|
|
profile_chunk_len: 0,
|
|
policy_leading_f32_0: 368.0,
|
|
policy_leading_f32_1: 0.0,
|
|
policy_leading_f32_2: 92.0,
|
|
policy_reserved_dwords: Vec::new(),
|
|
policy_trailing_word: 0,
|
|
policy_trailing_word_hex: "0x0000".to_string(),
|
|
profile_collection: Some(SmpSaveRegionProfileCollectionProbe {
|
|
direct_collection_flag: 1,
|
|
entry_stride: 0x22,
|
|
live_id_bound: 17,
|
|
live_record_count: 17,
|
|
entry_start_relative_offset: 0,
|
|
trailing_padding_len: 0,
|
|
entries: Vec::new(),
|
|
}),
|
|
}],
|
|
evidence: Vec::new(),
|
|
});
|
|
|
|
let trace = build_region_service_trace_report(&analysis);
|
|
assert_eq!(trace.region_record_triplet_count, 1);
|
|
assert_eq!(trace.queued_notice_record_count, 0);
|
|
assert!(!trace.atlas_candidate_consumers.is_empty());
|
|
assert_eq!(trace.known_owner_bridge_fields.len(), 6);
|
|
assert_eq!(trace.known_bridge_helpers.len(), 11);
|
|
assert_eq!(trace.next_owner_questions.len(), 3);
|
|
assert_eq!(trace.candidate_consumer_hypotheses.len(), 4);
|
|
assert_eq!(
|
|
trace.candidate_consumer_hypotheses[0].status,
|
|
"highest_priority_static_mapping_target"
|
|
);
|
|
assert_eq!(
|
|
trace.candidate_consumer_hypotheses[1].status,
|
|
"parallel_static_mapping_target"
|
|
);
|
|
assert_eq!(trace.entries.len(), 1);
|
|
assert_eq!(
|
|
trace.entries[0].branches[0].status,
|
|
"blocked_missing_pending_bonus_owner_lane"
|
|
);
|
|
assert_eq!(
|
|
trace.entries[0].branches[1].status,
|
|
"blocked_missing_completion_and_one_shot_latches"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn builds_infrastructure_asset_trace_report_with_alias_disproved_status() {
|
|
let mut analysis = empty_analysis_report();
|
|
analysis.placed_structure_record_triplets =
|
|
Some(SmpSavePlacedStructureRecordTripletProbe {
|
|
profile_family: analysis.profile_family.clone(),
|
|
source_kind: "save-placed-structure-triplets".to_string(),
|
|
semantic_family: "placed-structure-triplets".to_string(),
|
|
records_tag_offset: 0,
|
|
close_tag_offset: 0,
|
|
record_count: 2057,
|
|
entries: Vec::new(),
|
|
evidence: Vec::new(),
|
|
});
|
|
analysis.placed_structure_dynamic_side_buffer =
|
|
Some(SmpSavePlacedStructureDynamicSideBufferProbe {
|
|
profile_family: analysis.profile_family.clone(),
|
|
source_kind: "save-side-buffer".to_string(),
|
|
semantic_family: "infrastructure-asset".to_string(),
|
|
metadata_tag_offset: 0,
|
|
records_tag_offset: 0,
|
|
close_tag_offset: 0,
|
|
records_span_len: 0,
|
|
direct_record_stride: 6,
|
|
direct_record_stride_hex: "0x00000006".to_string(),
|
|
live_id_bound: 3865,
|
|
live_id_bound_hex: "0x00000f19".to_string(),
|
|
live_record_count: 3865,
|
|
live_record_count_hex: "0x00000f19".to_string(),
|
|
owner_shared_dword: 0xff000000,
|
|
owner_shared_dword_hex: "0xff000000".to_string(),
|
|
owner_shared_dword_relative_offset: 0,
|
|
owner_shared_dword_matches_first_compact_prefix_leading_dword: true,
|
|
prefix_leading_dword: 0xff000000,
|
|
prefix_leading_dword_hex: "0xff000000".to_string(),
|
|
prefix_trailing_word: 1,
|
|
prefix_trailing_word_hex: "0x0001".to_string(),
|
|
prefix_separator_byte: 0xff,
|
|
prefix_separator_byte_hex: "0xff".to_string(),
|
|
first_embedded_name_tag_relative_offset: 0x20,
|
|
embedded_name_tag_count: 138,
|
|
decoded_embedded_name_row_count: 138,
|
|
decoded_embedded_name_row_with_tertiary_name_count: 0,
|
|
unique_compact_prefix_pattern_count: 7,
|
|
prefix_leading_dword_matching_embedded_profile_tag_count: 17,
|
|
unique_embedded_name_pair_count: 5,
|
|
first_embedded_primary_name: Some("TrackCapST_Cap.3dp".to_string()),
|
|
first_embedded_secondary_name: Some("Infrastructure".to_string()),
|
|
first_embedded_tertiary_name: None,
|
|
embedded_name_row_samples: Vec::new(),
|
|
compact_prefix_pattern_summaries: Vec::new(),
|
|
name_pair_summaries: vec![SmpSavePlacedStructureDynamicSideBufferNamePairSummary {
|
|
primary_name: "TrackCapST_Cap.3dp".to_string(),
|
|
secondary_name: "Infrastructure".to_string(),
|
|
count: 12,
|
|
first_name_tag_relative_offset: 0x20,
|
|
unique_compact_prefix_pattern_count: 2,
|
|
dominant_prefix_leading_dword: 0xff0000ff,
|
|
dominant_prefix_leading_dword_hex: "0xff0000ff".to_string(),
|
|
dominant_prefix_trailing_word: 1,
|
|
dominant_prefix_trailing_word_hex: "0x0001".to_string(),
|
|
dominant_prefix_separator_byte: 0xff,
|
|
dominant_prefix_separator_byte_hex: "0xff".to_string(),
|
|
dominant_prefix_count: 9,
|
|
}],
|
|
evidence: Vec::new(),
|
|
});
|
|
analysis.placed_structure_dynamic_side_buffer_alignment =
|
|
Some(SmpSavePlacedStructureDynamicSideBufferAlignmentProbe {
|
|
unique_side_buffer_name_pair_count: 5,
|
|
unique_triplet_name_pair_count: 56,
|
|
overlapping_name_pair_count: 0,
|
|
side_buffer_row_count: 138,
|
|
side_buffer_rows_with_matching_triplet_name_pair_count: 0,
|
|
side_buffer_rows_without_matching_triplet_name_pair_count: 138,
|
|
triplet_name_pairs_without_side_buffer_match_count: 56,
|
|
matched_name_pair_samples: Vec::new(),
|
|
unmatched_side_buffer_name_pair_samples: Vec::new(),
|
|
evidence: Vec::new(),
|
|
});
|
|
|
|
let trace = build_infrastructure_asset_trace_report(&analysis);
|
|
assert!(trace.side_buffer_present);
|
|
assert_eq!(trace.triplet_alignment_overlap_count, 0);
|
|
assert_eq!(trace.known_owner_bridge_fields.len(), 5);
|
|
assert_eq!(trace.known_bridge_helpers.len(), 12);
|
|
assert_eq!(trace.next_owner_questions.len(), 3);
|
|
assert_eq!(trace.candidate_consumer_hypotheses.len(), 3);
|
|
assert_eq!(
|
|
trace.candidate_consumer_hypotheses[0].status,
|
|
"highest_priority_static_mapping_target"
|
|
);
|
|
assert!(
|
|
trace.candidate_consumer_hypotheses[0]
|
|
.evidence
|
|
.iter()
|
|
.any(|line| {
|
|
line.contains("0x00518140")
|
|
&& line.contains("12-byte row")
|
|
&& line.contains("[collection+0x3c]")
|
|
})
|
|
);
|
|
assert!(
|
|
trace.candidate_consumer_hypotheses[0]
|
|
.evidence
|
|
.iter()
|
|
.any(|line| line.contains("0x00518380") && line.contains("ordinal"))
|
|
);
|
|
assert!(
|
|
trace.candidate_consumer_hypotheses[0]
|
|
.evidence
|
|
.iter()
|
|
.any(|line| line.contains("0x005181f0/0x00518260")
|
|
&& line.contains("previous live id"))
|
|
);
|
|
assert_eq!(trace.branches[0].status, "grounded_separate_owner_seam");
|
|
assert_eq!(trace.branches[1].status, "disproved_by_grounded_probe");
|
|
}
|
|
}
|