2026-04-11 18:12:25 -07:00
|
|
|
use std::collections::BTreeMap;
|
2026-04-10 01:22:47 -07:00
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
2026-04-11 18:12:25 -07:00
|
|
|
use crate::{
|
2026-04-14 20:01:43 -07:00
|
|
|
CalendarPoint, RuntimePackedEventCollectionSummary, RuntimeSaveProfileState,
|
|
|
|
|
RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, SmpLoadedSaveSlice,
|
2026-04-11 18:12:25 -07:00
|
|
|
};
|
2026-04-10 01:22:47 -07:00
|
|
|
|
|
|
|
|
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct RuntimeStateDumpSource {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub source_binary: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimeStateDumpDocument {
|
|
|
|
|
pub format_version: u32,
|
|
|
|
|
pub dump_id: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub source: RuntimeStateDumpSource,
|
|
|
|
|
pub state: RuntimeState,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct RuntimeStateImport {
|
|
|
|
|
pub import_id: String,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub state: RuntimeState,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:12:25 -07:00
|
|
|
pub fn project_save_slice_to_runtime_state_import(
|
|
|
|
|
save_slice: &SmpLoadedSaveSlice,
|
|
|
|
|
import_id: &str,
|
|
|
|
|
description: Option<String>,
|
|
|
|
|
) -> Result<RuntimeStateImport, String> {
|
|
|
|
|
if import_id.trim().is_empty() {
|
|
|
|
|
return Err("import_id must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut world_flags = BTreeMap::new();
|
|
|
|
|
world_flags.insert(
|
|
|
|
|
"save_slice.profile_present".to_string(),
|
|
|
|
|
save_slice.profile.is_some(),
|
|
|
|
|
);
|
|
|
|
|
world_flags.insert(
|
|
|
|
|
"save_slice.candidate_availability_present".to_string(),
|
|
|
|
|
save_slice.candidate_availability_table.is_some(),
|
|
|
|
|
);
|
|
|
|
|
world_flags.insert(
|
|
|
|
|
"save_slice.special_conditions_present".to_string(),
|
|
|
|
|
save_slice.special_conditions_table.is_some(),
|
|
|
|
|
);
|
2026-04-14 20:01:43 -07:00
|
|
|
world_flags.insert(
|
|
|
|
|
"save_slice.event_runtime_collection_present".to_string(),
|
|
|
|
|
save_slice.event_runtime_collection.is_some(),
|
|
|
|
|
);
|
2026-04-11 18:12:25 -07:00
|
|
|
world_flags.insert(
|
|
|
|
|
"save_slice.mechanism_confidence_grounded".to_string(),
|
|
|
|
|
save_slice.mechanism_confidence == "grounded",
|
|
|
|
|
);
|
|
|
|
|
if let Some(profile) = &save_slice.profile {
|
|
|
|
|
world_flags.insert(
|
|
|
|
|
"save_slice.profile_byte_0x82_nonzero".to_string(),
|
|
|
|
|
profile.profile_byte_0x82 != 0,
|
|
|
|
|
);
|
|
|
|
|
world_flags.insert(
|
|
|
|
|
"save_slice.profile_byte_0x97_nonzero".to_string(),
|
|
|
|
|
profile.profile_byte_0x97 != 0,
|
|
|
|
|
);
|
|
|
|
|
world_flags.insert(
|
|
|
|
|
"save_slice.profile_byte_0xc5_nonzero".to_string(),
|
|
|
|
|
profile.profile_byte_0xc5 != 0,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut metadata = BTreeMap::new();
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.import_projection".to_string(),
|
|
|
|
|
"partial-runtime-restore-v1".to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.calendar_source".to_string(),
|
|
|
|
|
"default-1830-placeholder".to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.selected_year_seed_tuple_source".to_string(),
|
|
|
|
|
"raw-lane-via-0x51d3f0".to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.selected_year_absolute_counter_source".to_string(),
|
|
|
|
|
"mode-adjusted-lane-via-0x51d390-0x409e80".to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.selected_year_absolute_counter_reconstructible_from_save".to_string(),
|
|
|
|
|
"false".to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.disable_cargo_economy_special_condition_slot".to_string(),
|
|
|
|
|
"30".to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.disable_cargo_economy_special_condition_reconstructible_from_save".to_string(),
|
|
|
|
|
"true".to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.disable_cargo_economy_special_condition_write_side_grounded".to_string(),
|
|
|
|
|
"true".to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.selected_year_absolute_counter_adjustment_context".to_string(),
|
|
|
|
|
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30"
|
|
|
|
|
.to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.mechanism_family".to_string(),
|
|
|
|
|
save_slice.mechanism_family.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.mechanism_confidence".to_string(),
|
|
|
|
|
save_slice.mechanism_confidence.clone(),
|
|
|
|
|
);
|
|
|
|
|
if let Some(family) = &save_slice.container_profile_family {
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.container_profile_family".to_string(),
|
|
|
|
|
family.clone(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if let Some(family) = &save_slice.trailer_family {
|
|
|
|
|
metadata.insert("save_slice.trailer_family".to_string(), family.clone());
|
|
|
|
|
}
|
|
|
|
|
if let Some(family) = &save_slice.bridge_family {
|
|
|
|
|
metadata.insert("save_slice.bridge_family".to_string(), family.clone());
|
|
|
|
|
}
|
2026-04-14 20:01:43 -07:00
|
|
|
let packed_event_collection = save_slice.event_runtime_collection.as_ref().map(|summary| {
|
|
|
|
|
RuntimePackedEventCollectionSummary {
|
|
|
|
|
source_kind: summary.source_kind.clone(),
|
|
|
|
|
mechanism_family: summary.mechanism_family.clone(),
|
|
|
|
|
mechanism_confidence: summary.mechanism_confidence.clone(),
|
|
|
|
|
container_profile_family: summary.container_profile_family.clone(),
|
|
|
|
|
packed_state_version: summary.packed_state_version,
|
|
|
|
|
packed_state_version_hex: summary.packed_state_version_hex.clone(),
|
|
|
|
|
live_id_bound: summary.live_id_bound,
|
|
|
|
|
live_record_count: summary.live_record_count,
|
|
|
|
|
live_entry_ids: summary.live_entry_ids.clone(),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if let Some(summary) = &save_slice.event_runtime_collection {
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.event_runtime_collection_source_kind".to_string(),
|
|
|
|
|
summary.source_kind.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.event_runtime_collection_version_hex".to_string(),
|
|
|
|
|
summary.packed_state_version_hex.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.event_runtime_collection_record_count".to_string(),
|
|
|
|
|
summary.live_record_count.to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-11 18:12:25 -07:00
|
|
|
let save_profile = if let Some(profile) = &save_slice.profile {
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.profile_kind".to_string(),
|
|
|
|
|
profile.profile_kind.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.profile_family".to_string(),
|
|
|
|
|
profile.profile_family.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.packed_profile_offset".to_string(),
|
|
|
|
|
profile.packed_profile_offset.to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.packed_profile_len".to_string(),
|
|
|
|
|
profile.packed_profile_len.to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.leading_word_0_hex".to_string(),
|
|
|
|
|
profile.leading_word_0_hex.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.profile_byte_0x77_hex".to_string(),
|
|
|
|
|
profile.profile_byte_0x77_hex.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.profile_byte_0x82_hex".to_string(),
|
|
|
|
|
profile.profile_byte_0x82_hex.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.profile_byte_0x97_hex".to_string(),
|
|
|
|
|
profile.profile_byte_0x97_hex.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.profile_byte_0xc5_hex".to_string(),
|
|
|
|
|
profile.profile_byte_0xc5_hex.clone(),
|
|
|
|
|
);
|
|
|
|
|
if let Some(header_flag_word_3_hex) = &profile.header_flag_word_3_hex {
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.header_flag_word_3_hex".to_string(),
|
|
|
|
|
header_flag_word_3_hex.clone(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if let Some(map_path) = &profile.map_path {
|
|
|
|
|
metadata.insert("save_slice.map_path".to_string(), map_path.clone());
|
|
|
|
|
}
|
|
|
|
|
if let Some(display_name) = &profile.display_name {
|
|
|
|
|
metadata.insert("save_slice.display_name".to_string(), display_name.clone());
|
|
|
|
|
}
|
|
|
|
|
RuntimeSaveProfileState {
|
|
|
|
|
profile_kind: Some(profile.profile_kind.clone()),
|
|
|
|
|
profile_family: Some(profile.profile_family.clone()),
|
|
|
|
|
map_path: profile.map_path.clone(),
|
|
|
|
|
display_name: profile.display_name.clone(),
|
|
|
|
|
selected_year_profile_lane: Some(profile.profile_byte_0x77),
|
|
|
|
|
sandbox_enabled: Some(profile.profile_byte_0x82 != 0),
|
|
|
|
|
campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0),
|
|
|
|
|
staged_profile_copy_on_restore: Some(profile.profile_byte_0x97 != 0),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
RuntimeSaveProfileState::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let special_condition_enabled = |slot_index: u8| {
|
|
|
|
|
save_slice.special_conditions_table.as_ref().map(|table| {
|
|
|
|
|
table
|
|
|
|
|
.entries
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|entry| entry.slot_index == slot_index)
|
|
|
|
|
.map(|entry| entry.value != 0)
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let world_restore = if let Some(profile) = &save_slice.profile {
|
|
|
|
|
let disable_cargo_economy_special_condition_enabled = special_condition_enabled(30);
|
|
|
|
|
RuntimeWorldRestoreState {
|
|
|
|
|
selected_year_profile_lane: Some(profile.profile_byte_0x77),
|
|
|
|
|
campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0),
|
|
|
|
|
sandbox_enabled: Some(profile.profile_byte_0x82 != 0),
|
|
|
|
|
seed_tuple_written_from_raw_lane: Some(true),
|
|
|
|
|
absolute_counter_requires_shell_context: Some(true),
|
|
|
|
|
absolute_counter_reconstructible_from_save: Some(false),
|
|
|
|
|
disable_cargo_economy_special_condition_slot: Some(30),
|
|
|
|
|
disable_cargo_economy_special_condition_reconstructible_from_save: Some(true),
|
|
|
|
|
disable_cargo_economy_special_condition_write_side_grounded: Some(true),
|
|
|
|
|
disable_cargo_economy_special_condition_enabled,
|
|
|
|
|
use_bio_accelerator_cars_enabled: special_condition_enabled(29),
|
|
|
|
|
use_wartime_cargos_enabled: special_condition_enabled(31),
|
|
|
|
|
disable_train_crashes_enabled: special_condition_enabled(32),
|
|
|
|
|
disable_train_crashes_and_breakdowns_enabled: special_condition_enabled(33),
|
|
|
|
|
ai_ignore_territories_at_startup_enabled: special_condition_enabled(34),
|
|
|
|
|
absolute_counter_restore_kind: Some(
|
|
|
|
|
"mode-adjusted-selected-year-lane".to_string(),
|
|
|
|
|
),
|
|
|
|
|
absolute_counter_adjustment_context: Some(
|
|
|
|
|
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30"
|
|
|
|
|
.to_string(),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
RuntimeWorldRestoreState::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut candidate_availability = BTreeMap::new();
|
|
|
|
|
if let Some(table) = &save_slice.candidate_availability_table {
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.candidate_table_source_kind".to_string(),
|
|
|
|
|
table.source_kind.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.candidate_table_semantic_family".to_string(),
|
|
|
|
|
table.semantic_family.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.candidate_table_entry_count".to_string(),
|
|
|
|
|
table.observed_entry_count.to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.candidate_table_zero_count".to_string(),
|
|
|
|
|
table.zero_availability_count.to_string(),
|
|
|
|
|
);
|
|
|
|
|
for entry in &table.entries {
|
|
|
|
|
candidate_availability.insert(entry.text.clone(), entry.availability_dword);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let mut special_conditions = BTreeMap::new();
|
|
|
|
|
if let Some(table) = &save_slice.special_conditions_table {
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.special_conditions_source_kind".to_string(),
|
|
|
|
|
table.source_kind.clone(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.special_conditions_table_offset".to_string(),
|
|
|
|
|
table.table_offset.to_string(),
|
|
|
|
|
);
|
|
|
|
|
metadata.insert(
|
|
|
|
|
"save_slice.special_conditions_enabled_visible_count".to_string(),
|
|
|
|
|
table.enabled_visible_count.to_string(),
|
|
|
|
|
);
|
|
|
|
|
for entry in &table.entries {
|
|
|
|
|
if !entry.hidden {
|
|
|
|
|
special_conditions.insert(entry.label.clone(), entry.value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (index, note) in save_slice.notes.iter().enumerate() {
|
|
|
|
|
metadata.insert(format!("save_slice.note.{index}"), note.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let state = RuntimeState {
|
|
|
|
|
calendar: CalendarPoint {
|
|
|
|
|
year: 1830,
|
|
|
|
|
month_slot: 0,
|
|
|
|
|
phase_slot: 0,
|
|
|
|
|
tick_slot: 0,
|
|
|
|
|
},
|
|
|
|
|
world_flags,
|
|
|
|
|
save_profile,
|
|
|
|
|
world_restore,
|
|
|
|
|
metadata,
|
|
|
|
|
companies: Vec::new(),
|
2026-04-14 20:01:43 -07:00
|
|
|
packed_event_collection,
|
2026-04-11 18:12:25 -07:00
|
|
|
event_runtime_records: Vec::new(),
|
|
|
|
|
candidate_availability,
|
|
|
|
|
special_conditions,
|
|
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
};
|
|
|
|
|
state.validate()?;
|
|
|
|
|
|
|
|
|
|
Ok(RuntimeStateImport {
|
|
|
|
|
import_id: import_id.to_string(),
|
|
|
|
|
description,
|
|
|
|
|
state,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 01:22:47 -07:00
|
|
|
pub fn validate_runtime_state_dump_document(
|
|
|
|
|
document: &RuntimeStateDumpDocument,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
if document.format_version != STATE_DUMP_FORMAT_VERSION {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"unsupported state dump format_version {} (expected {})",
|
|
|
|
|
document.format_version, STATE_DUMP_FORMAT_VERSION
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if document.dump_id.trim().is_empty() {
|
|
|
|
|
return Err("dump_id must not be empty".to_string());
|
|
|
|
|
}
|
|
|
|
|
document.state.validate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load_runtime_state_import(
|
|
|
|
|
path: &Path,
|
|
|
|
|
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
|
|
|
|
let text = std::fs::read_to_string(path)?;
|
|
|
|
|
load_runtime_state_import_from_str(
|
|
|
|
|
&text,
|
|
|
|
|
path.file_stem()
|
|
|
|
|
.and_then(|stem| stem.to_str())
|
|
|
|
|
.unwrap_or("runtime-state"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load_runtime_state_import_from_str(
|
|
|
|
|
text: &str,
|
|
|
|
|
fallback_id: &str,
|
|
|
|
|
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
|
|
|
|
|
if let Ok(document) = serde_json::from_str::<RuntimeStateDumpDocument>(text) {
|
|
|
|
|
validate_runtime_state_dump_document(&document)
|
|
|
|
|
.map_err(|err| format!("invalid runtime state dump document: {err}"))?;
|
|
|
|
|
return Ok(RuntimeStateImport {
|
|
|
|
|
import_id: document.dump_id,
|
|
|
|
|
description: document.source.description,
|
|
|
|
|
state: document.state,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let state: RuntimeState = serde_json::from_str(text)?;
|
|
|
|
|
state
|
|
|
|
|
.validate()
|
|
|
|
|
.map_err(|err| format!("invalid runtime state: {err}"))?;
|
|
|
|
|
Ok(RuntimeStateImport {
|
|
|
|
|
import_id: fallback_id.to_string(),
|
|
|
|
|
description: None,
|
|
|
|
|
state,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn state() -> RuntimeState {
|
|
|
|
|
RuntimeState {
|
|
|
|
|
calendar: CalendarPoint {
|
|
|
|
|
year: 1830,
|
|
|
|
|
month_slot: 0,
|
|
|
|
|
phase_slot: 0,
|
|
|
|
|
tick_slot: 0,
|
|
|
|
|
},
|
|
|
|
|
world_flags: BTreeMap::new(),
|
2026-04-11 18:12:25 -07:00
|
|
|
save_profile: RuntimeSaveProfileState::default(),
|
|
|
|
|
world_restore: RuntimeWorldRestoreState::default(),
|
|
|
|
|
metadata: BTreeMap::new(),
|
2026-04-10 01:22:47 -07:00
|
|
|
companies: Vec::new(),
|
2026-04-14 20:01:43 -07:00
|
|
|
packed_event_collection: None,
|
2026-04-10 01:22:47 -07:00
|
|
|
event_runtime_records: Vec::new(),
|
2026-04-11 18:12:25 -07:00
|
|
|
candidate_availability: BTreeMap::new(),
|
|
|
|
|
special_conditions: BTreeMap::new(),
|
2026-04-10 01:22:47 -07:00
|
|
|
service_state: RuntimeServiceState::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn loads_dump_document() {
|
|
|
|
|
let text = serde_json::to_string(&RuntimeStateDumpDocument {
|
|
|
|
|
format_version: STATE_DUMP_FORMAT_VERSION,
|
|
|
|
|
dump_id: "dump-smoke".to_string(),
|
|
|
|
|
source: RuntimeStateDumpSource {
|
|
|
|
|
description: Some("test dump".to_string()),
|
|
|
|
|
source_binary: None,
|
|
|
|
|
},
|
|
|
|
|
state: state(),
|
|
|
|
|
})
|
|
|
|
|
.expect("dump should serialize");
|
|
|
|
|
|
|
|
|
|
let import =
|
|
|
|
|
load_runtime_state_import_from_str(&text, "fallback").expect("dump should load");
|
|
|
|
|
assert_eq!(import.import_id, "dump-smoke");
|
|
|
|
|
assert_eq!(import.description.as_deref(), Some("test dump"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn loads_bare_runtime_state() {
|
|
|
|
|
let text = serde_json::to_string(&state()).expect("state should serialize");
|
|
|
|
|
let import =
|
|
|
|
|
load_runtime_state_import_from_str(&text, "fallback").expect("state should load");
|
|
|
|
|
assert_eq!(import.import_id, "fallback");
|
|
|
|
|
assert!(import.description.is_none());
|
|
|
|
|
}
|
2026-04-11 18:12:25 -07:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn projects_save_slice_into_runtime_state_import() {
|
|
|
|
|
let save_slice = SmpLoadedSaveSlice {
|
|
|
|
|
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(),
|
|
|
|
|
trailer_family: Some("rt3-105-save-trailer-v1".to_string()),
|
|
|
|
|
bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()),
|
|
|
|
|
profile: Some(crate::SmpLoadedProfile {
|
|
|
|
|
profile_kind: "rt3-105-packed-profile".to_string(),
|
|
|
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
|
|
|
|
packed_profile_offset: 0x73c0,
|
|
|
|
|
packed_profile_len: 0x108,
|
|
|
|
|
packed_profile_len_hex: "0x108".to_string(),
|
|
|
|
|
leading_word_0: 3,
|
|
|
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
|
|
|
|
header_flag_word_3: Some(0x01000000),
|
|
|
|
|
header_flag_word_3_hex: Some("0x01000000".to_string()),
|
|
|
|
|
map_path: Some("Alternate USA.gmp".to_string()),
|
|
|
|
|
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: 0x00,
|
|
|
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
|
|
|
|
profile_byte_0xc5: 0x00,
|
|
|
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
|
|
|
|
}),
|
|
|
|
|
candidate_availability_table: Some(crate::SmpLoadedCandidateAvailabilityTable {
|
|
|
|
|
source_kind: "save-bridge-secondary-block".to_string(),
|
|
|
|
|
semantic_family: "scenario-named-candidate-availability-table".to_string(),
|
|
|
|
|
header_offset: 0x6a70,
|
|
|
|
|
entries_offset: 0x6ad1,
|
|
|
|
|
entries_end_offset: 0x73b7,
|
|
|
|
|
observed_entry_count: 2,
|
|
|
|
|
zero_availability_count: 1,
|
|
|
|
|
zero_availability_names: vec!["Uranium Mine".to_string()],
|
|
|
|
|
footer_progress_hex_words: vec!["0x000032dc".to_string(), "0x00003714".to_string()],
|
|
|
|
|
entries: vec![
|
|
|
|
|
crate::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(),
|
|
|
|
|
},
|
|
|
|
|
crate::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(),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
special_conditions_table: Some(crate::SmpLoadedSpecialConditionsTable {
|
|
|
|
|
source_kind: "save-fixed-special-conditions-range".to_string(),
|
|
|
|
|
table_offset: 0x0d64,
|
|
|
|
|
table_len: 36 * 4,
|
|
|
|
|
enabled_visible_count: 0,
|
|
|
|
|
enabled_visible_labels: vec![],
|
|
|
|
|
entries: vec![
|
|
|
|
|
crate::SmpSpecialConditionEntry {
|
|
|
|
|
slot_index: 30,
|
|
|
|
|
hidden: false,
|
|
|
|
|
label_id: 3722,
|
|
|
|
|
help_id: 3723,
|
|
|
|
|
label: "Disable Cargo Economy".to_string(),
|
|
|
|
|
value: 0,
|
|
|
|
|
value_hex: "0x00000000".to_string(),
|
|
|
|
|
},
|
|
|
|
|
crate::SmpSpecialConditionEntry {
|
|
|
|
|
slot_index: 35,
|
|
|
|
|
hidden: true,
|
|
|
|
|
label_id: 3,
|
|
|
|
|
help_id: 3,
|
|
|
|
|
label: "Hidden sentinel".to_string(),
|
|
|
|
|
value: 1,
|
|
|
|
|
value_hex: "0x00000001".to_string(),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}),
|
2026-04-14 20:01:43 -07:00
|
|
|
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
|
|
|
|
|
source_kind: "packed-event-runtime-collection".to_string(),
|
|
|
|
|
mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
|
|
|
|
mechanism_confidence: "mixed".to_string(),
|
|
|
|
|
container_profile_family: Some("rt3-105-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],
|
|
|
|
|
}),
|
2026-04-11 18:12:25 -07:00
|
|
|
notes: vec!["packed profile recovered".to_string()],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let import = project_save_slice_to_runtime_state_import(
|
|
|
|
|
&save_slice,
|
|
|
|
|
"save-import-smoke",
|
|
|
|
|
Some("test save import".to_string()),
|
|
|
|
|
)
|
|
|
|
|
.expect("save slice should project");
|
|
|
|
|
|
|
|
|
|
assert_eq!(import.import_id, "save-import-smoke");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.metadata
|
|
|
|
|
.get("save_slice.map_path")
|
|
|
|
|
.map(String::as_str),
|
|
|
|
|
Some("Alternate USA.gmp")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.save_profile.selected_year_profile_lane,
|
|
|
|
|
Some(0x07)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(import.state.save_profile.sandbox_enabled, Some(true));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.world_restore.selected_year_profile_lane,
|
|
|
|
|
Some(0x07)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(import.state.world_restore.sandbox_enabled, Some(true));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.world_restore.campaign_scenario_enabled,
|
|
|
|
|
Some(false)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.world_restore.seed_tuple_written_from_raw_lane,
|
|
|
|
|
Some(true)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.absolute_counter_requires_shell_context,
|
|
|
|
|
Some(true)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.absolute_counter_reconstructible_from_save,
|
|
|
|
|
Some(false)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.disable_cargo_economy_special_condition_slot,
|
|
|
|
|
Some(30)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.disable_cargo_economy_special_condition_reconstructible_from_save,
|
|
|
|
|
Some(true)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.disable_cargo_economy_special_condition_write_side_grounded,
|
|
|
|
|
Some(true)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.disable_cargo_economy_special_condition_enabled,
|
|
|
|
|
Some(false)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.world_restore.use_bio_accelerator_cars_enabled,
|
|
|
|
|
Some(false)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.world_restore.use_wartime_cargos_enabled,
|
|
|
|
|
Some(false)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.world_restore.disable_train_crashes_enabled,
|
|
|
|
|
Some(false)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.disable_train_crashes_and_breakdowns_enabled,
|
|
|
|
|
Some(false)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.ai_ignore_territories_at_startup_enabled,
|
|
|
|
|
Some(false)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.absolute_counter_restore_kind
|
|
|
|
|
.as_deref(),
|
|
|
|
|
Some("mode-adjusted-selected-year-lane")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_restore
|
|
|
|
|
.absolute_counter_adjustment_context
|
|
|
|
|
.as_deref(),
|
|
|
|
|
Some(
|
|
|
|
|
"editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30"
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.save_profile.map_path.as_deref(),
|
|
|
|
|
Some("Alternate USA.gmp")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.candidate_availability.get("Uranium Mine"),
|
|
|
|
|
Some(&0)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import.state.special_conditions.get("Disable Cargo Economy"),
|
|
|
|
|
Some(&0)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.world_flags
|
|
|
|
|
.get("save_slice.profile_byte_0x82_nonzero"),
|
|
|
|
|
Some(&true)
|
|
|
|
|
);
|
2026-04-14 20:01:43 -07:00
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.packed_event_collection
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|summary| summary.live_record_count),
|
|
|
|
|
Some(3)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
import
|
|
|
|
|
.state
|
|
|
|
|
.packed_event_collection
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|summary| summary.live_entry_ids.clone()),
|
|
|
|
|
Some(vec![1, 3, 5])
|
|
|
|
|
);
|
|
|
|
|
assert!(import.state.event_runtime_records.is_empty());
|
2026-04-11 18:12:25 -07:00
|
|
|
}
|
2026-04-10 01:22:47 -07:00
|
|
|
}
|