Commit runtime loader and atlas updates
This commit is contained in:
parent
1040a131da
commit
b173c50c1a
19 changed files with 8425 additions and 698 deletions
|
|
@ -1,8 +1,12 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::RuntimeState;
|
||||
use crate::{
|
||||
CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
|
||||
RuntimeWorldRestoreState, SmpLoadedSaveSlice,
|
||||
};
|
||||
|
||||
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
|
||||
|
||||
|
|
@ -30,6 +34,282 @@ pub struct RuntimeStateImport {
|
|||
pub state: RuntimeState,
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
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());
|
||||
}
|
||||
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(),
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_runtime_state_dump_document(
|
||||
document: &RuntimeStateDumpDocument,
|
||||
) -> Result<(), String> {
|
||||
|
|
@ -85,8 +365,6 @@ pub fn load_runtime_state_import_from_str(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{CalendarPoint, RuntimeServiceState};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn state() -> RuntimeState {
|
||||
RuntimeState {
|
||||
|
|
@ -97,8 +375,13 @@ mod tests {
|
|||
tick_slot: 0,
|
||||
},
|
||||
world_flags: BTreeMap::new(),
|
||||
save_profile: RuntimeSaveProfileState::default(),
|
||||
world_restore: RuntimeWorldRestoreState::default(),
|
||||
metadata: BTreeMap::new(),
|
||||
companies: Vec::new(),
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: BTreeMap::new(),
|
||||
special_conditions: BTreeMap::new(),
|
||||
service_state: RuntimeServiceState::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -130,4 +413,236 @@ mod tests {
|
|||
assert_eq!(import.import_id, "fallback");
|
||||
assert!(import.description.is_none());
|
||||
}
|
||||
|
||||
#[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(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue