Ground named cargo production event descriptors
This commit is contained in:
parent
358d4cdec3
commit
6a5d028d19
14 changed files with 987 additions and 217 deletions
|
|
@ -119,10 +119,49 @@ pub fn write_indexed_collection_probe(
|
|||
Ok(path)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CargoCollectionProbeRow {
|
||||
pub entry_id: usize,
|
||||
pub live: bool,
|
||||
pub resolved_ptr: usize,
|
||||
pub stem: Option<String>,
|
||||
pub route_style_byte: Option<u8>,
|
||||
pub subtype_byte: Option<u8>,
|
||||
pub class_marker: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CargoCollectionProbe {
|
||||
pub collection_addr: usize,
|
||||
pub flat_payload: bool,
|
||||
pub stride: u32,
|
||||
pub id_bound: i32,
|
||||
pub payload_ptr: usize,
|
||||
pub tombstone_ptr: usize,
|
||||
pub live_entry_count: usize,
|
||||
pub rows: Vec<CargoCollectionProbeRow>,
|
||||
}
|
||||
|
||||
pub fn write_cargo_collection_probe(
|
||||
base_dir: &Path,
|
||||
stem: &str,
|
||||
probe: &CargoCollectionProbe,
|
||||
) -> io::Result<PathBuf> {
|
||||
fs::create_dir_all(base_dir)?;
|
||||
|
||||
let path = base_dir.join(format!("rrt_cargo_{stem}_collection_probe.json"));
|
||||
let json = serde_json::to_vec_pretty(probe)
|
||||
.map_err(|err| io::Error::other(format!("serialize cargo collection probe: {err}")))?;
|
||||
fs::write(&path, json)?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_hook {
|
||||
use super::{
|
||||
IndexedCollectionProbe, IndexedCollectionProbeRow, sample_finance_snapshot,
|
||||
CargoCollectionProbe, CargoCollectionProbeRow, IndexedCollectionProbe,
|
||||
IndexedCollectionProbeRow, sample_finance_snapshot, write_cargo_collection_probe,
|
||||
write_finance_snapshot_bundle, write_finance_snapshot_only, write_indexed_collection_probe,
|
||||
};
|
||||
use core::ffi::{c_char, c_void};
|
||||
|
|
@ -160,6 +199,9 @@ mod windows_hook {
|
|||
static FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE: &[u8] =
|
||||
b"rrt-hook: finance probe snapshot written\n";
|
||||
static FINANCE_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: finance capture timed out\n";
|
||||
static CARGO_CAPTURE_STARTED_MESSAGE: &[u8] = b"rrt-hook: cargo capture thread started\n";
|
||||
static CARGO_CAPTURE_WRITTEN_MESSAGE: &[u8] = b"rrt-hook: cargo collection probe written\n";
|
||||
static CARGO_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: cargo capture timed out\n";
|
||||
static AUTO_LOAD_STARTED_MESSAGE: &[u8] = b"rrt-hook: auto load hook armed\n";
|
||||
static AUTO_LOAD_HOOK_INSTALLED_MESSAGE: &[u8] =
|
||||
b"rrt-hook: auto load shell-state hook installed\n";
|
||||
|
|
@ -284,6 +326,7 @@ mod windows_hook {
|
|||
static FINANCE_TEMPLATE_EMITTED: AtomicBool = AtomicBool::new(false);
|
||||
static FINANCE_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
static FINANCE_COLLECTION_PROBE_WRITTEN: AtomicBool = AtomicBool::new(false);
|
||||
static CARGO_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
static AUTO_LOAD_THREAD_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
static AUTO_LOAD_HOOK_INSTALLED: AtomicBool = AtomicBool::new(false);
|
||||
static AUTO_LOAD_ATTEMPTED: AtomicBool = AtomicBool::new(false);
|
||||
|
|
@ -324,6 +367,7 @@ mod windows_hook {
|
|||
static mut MODE2_TEARDOWN_TRAMPOLINE: usize = 0;
|
||||
|
||||
const COMPANY_COLLECTION_ADDR: usize = 0x0062be10;
|
||||
const CARGO_COLLECTION_ADDR: usize = 0x0062ba8c;
|
||||
const SHELL_CONTROLLER_PTR_ADDR: usize = 0x006d4024;
|
||||
const SHELL_STATE_PTR_ADDR: usize = 0x006cec74;
|
||||
const ACTIVE_MODE_PTR_ADDR: usize = 0x006cec78;
|
||||
|
|
@ -360,6 +404,10 @@ mod windows_hook {
|
|||
const INDEXED_COLLECTION_ID_BOUND_OFFSET: usize = 0x14;
|
||||
const INDEXED_COLLECTION_PAYLOAD_OFFSET: usize = 0x30;
|
||||
const INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET: usize = 0x34;
|
||||
const CARGO_STEM_OFFSET: usize = 0x04;
|
||||
const CARGO_SUBTYPE_OFFSET: usize = 0x32;
|
||||
const CARGO_ROUTE_STYLE_OFFSET: usize = 0x46;
|
||||
const CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET: usize = 0x9a;
|
||||
const COMPANY_ACTIVE_OFFSET: usize = 0x3f;
|
||||
const COMPANY_OUTSTANDING_SHARES_OFFSET: usize = 0x47;
|
||||
const COMPANY_COMPANY_VALUE_OFFSET: usize = 0x57;
|
||||
|
|
@ -493,6 +541,7 @@ mod windows_hook {
|
|||
) -> i32 {
|
||||
maybe_emit_finance_template_bundle();
|
||||
maybe_start_finance_capture_thread();
|
||||
maybe_start_cargo_capture_thread();
|
||||
maybe_install_auto_load_hook();
|
||||
|
||||
let direct_input8_create = unsafe { load_direct_input8_create() };
|
||||
|
|
@ -592,6 +641,41 @@ mod windows_hook {
|
|||
});
|
||||
}
|
||||
|
||||
fn maybe_start_cargo_capture_thread() {
|
||||
if env::var_os("RRT_WRITE_CARGO_CAPTURE").is_none() {
|
||||
return;
|
||||
}
|
||||
if CARGO_CAPTURE_STARTED.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
|
||||
append_log_message(CARGO_CAPTURE_STARTED_MESSAGE);
|
||||
let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let _ = thread::Builder::new()
|
||||
.name("rrt-cargo-capture".to_string())
|
||||
.spawn(move || {
|
||||
let mut last_probe: Option<CargoCollectionProbe> = None;
|
||||
for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS {
|
||||
if let Some(probe) = unsafe { capture_cargo_collection_probe() } {
|
||||
last_probe = Some(probe.clone());
|
||||
if probe.live_entry_count > 0
|
||||
&& write_cargo_collection_probe(&base_dir, "attach_probe", &probe)
|
||||
.is_ok()
|
||||
{
|
||||
append_log_message(CARGO_CAPTURE_WRITTEN_MESSAGE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
thread::sleep(CAPTURE_POLL_INTERVAL);
|
||||
}
|
||||
|
||||
if let Some(probe) = last_probe {
|
||||
let _ = write_cargo_collection_probe(&base_dir, "attach_probe_timeout", &probe);
|
||||
}
|
||||
append_log_message(CARGO_CAPTURE_TIMEOUT_MESSAGE);
|
||||
});
|
||||
}
|
||||
|
||||
fn maybe_install_auto_load_hook() {
|
||||
let save_stem = match env::var("RRT_AUTO_LOAD_SAVE") {
|
||||
Ok(value) if !value.trim().is_empty() => value,
|
||||
|
|
@ -2751,6 +2835,93 @@ mod windows_hook {
|
|||
})
|
||||
}
|
||||
|
||||
unsafe fn capture_cargo_collection_probe() -> Option<CargoCollectionProbe> {
|
||||
let collection = CARGO_COLLECTION_ADDR as *const u8;
|
||||
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
|
||||
if id_bound <= 0 {
|
||||
return Some(CargoCollectionProbe {
|
||||
collection_addr: CARGO_COLLECTION_ADDR,
|
||||
flat_payload: unsafe {
|
||||
read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0
|
||||
},
|
||||
stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) },
|
||||
id_bound,
|
||||
payload_ptr: unsafe {
|
||||
read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize
|
||||
},
|
||||
tombstone_ptr: unsafe {
|
||||
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize
|
||||
},
|
||||
live_entry_count: 0,
|
||||
rows: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut live_entry_count = 0_usize;
|
||||
let mut rows = Vec::with_capacity(id_bound as usize);
|
||||
for entry_id in 1..=id_bound as usize {
|
||||
let live = unsafe { indexed_collection_entry_id_is_live(collection, entry_id) };
|
||||
let resolved_ptr = unsafe {
|
||||
indexed_collection_resolve_live_entry_by_id(collection, entry_id) as usize
|
||||
};
|
||||
if live && resolved_ptr != 0 {
|
||||
live_entry_count += 1;
|
||||
}
|
||||
let stem = if resolved_ptr == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe {
|
||||
read_c_string((resolved_ptr as *const u8).add(CARGO_STEM_OFFSET), 0x1e)
|
||||
})
|
||||
};
|
||||
let route_style_byte = if resolved_ptr == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_ROUTE_STYLE_OFFSET)) })
|
||||
};
|
||||
let subtype_byte = if resolved_ptr == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_SUBTYPE_OFFSET)) })
|
||||
};
|
||||
let class_marker = if live {
|
||||
Some(unsafe {
|
||||
read_u32(
|
||||
collection.add(CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET + entry_id * 4),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
rows.push(CargoCollectionProbeRow {
|
||||
entry_id,
|
||||
live,
|
||||
resolved_ptr,
|
||||
stem,
|
||||
route_style_byte,
|
||||
subtype_byte,
|
||||
class_marker,
|
||||
});
|
||||
}
|
||||
|
||||
Some(CargoCollectionProbe {
|
||||
collection_addr: CARGO_COLLECTION_ADDR,
|
||||
flat_payload: unsafe {
|
||||
read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0
|
||||
},
|
||||
stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) },
|
||||
id_bound,
|
||||
payload_ptr: unsafe {
|
||||
read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize
|
||||
},
|
||||
tombstone_ptr: unsafe {
|
||||
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize
|
||||
},
|
||||
live_entry_count,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn capture_probe_snapshot_from_company(company: *mut u8) -> FinanceSnapshot {
|
||||
let scenario = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as *const u8;
|
||||
let current_year = unsafe { read_u16(scenario.add(SCENARIO_CURRENT_YEAR_OFFSET)) };
|
||||
|
|
@ -2905,6 +3076,18 @@ mod windows_hook {
|
|||
unsafe { ptr::read_unaligned(address.cast::<*mut u8>()) }
|
||||
}
|
||||
|
||||
unsafe fn read_c_string(address: *const u8, max_len: usize) -> String {
|
||||
let mut len = 0;
|
||||
while len < max_len {
|
||||
let byte = unsafe { read_u8(address.add(len)) };
|
||||
if byte == 0 {
|
||||
break;
|
||||
}
|
||||
len += 1;
|
||||
}
|
||||
String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(address, len) }).into_owned()
|
||||
}
|
||||
|
||||
unsafe fn load_direct_input8_create() -> Option<DirectInput8CreateFn> {
|
||||
if let Some(callback) = unsafe { REAL_DINPUT8_CREATE } {
|
||||
return Some(callback);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ pub const REQUIRED_EXPORTS: &[&str] = &[
|
|||
"artifacts/exports/rt3-1.06/pending-template-store-record-kinds.csv",
|
||||
"artifacts/exports/rt3-1.06/pending-template-store-management.md",
|
||||
"artifacts/exports/rt3-1.06/event-effects-table.json",
|
||||
"artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json",
|
||||
"artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json",
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ enum ImportBlocker {
|
|||
NamedTerritoryBinding,
|
||||
UnmappedOrdinaryCondition,
|
||||
UnmappedWorldCondition,
|
||||
EvidenceBlockedDescriptor,
|
||||
MissingTrainContext,
|
||||
MissingTrainTerritoryContext,
|
||||
MissingLocomotiveCatalogContext,
|
||||
|
|
@ -1216,6 +1217,7 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
|
|||
.or_else(|| row.recovered_cargo_class.clone()),
|
||||
recovered_cargo_label: cargo_entry
|
||||
.map(|entry| entry.label.clone())
|
||||
.or_else(|| row.recovered_cargo_label.clone())
|
||||
.or_else(|| row.recovered_cargo_slot.map(default_cargo_slot_label)),
|
||||
recovered_cargo_supplied_token_stem: cargo_entry
|
||||
.and_then(|entry| entry.supplied_token_stem.clone()),
|
||||
|
|
@ -1488,6 +1490,15 @@ fn lower_contextual_cargo_production_effect(
|
|||
target: RuntimeCargoProductionTarget::FarmMine,
|
||||
value,
|
||||
})),
|
||||
180..=229 => {
|
||||
let Some(name) = row.recovered_cargo_label.clone() else {
|
||||
return Err(ImportBlocker::EvidenceBlockedDescriptor);
|
||||
};
|
||||
Ok(Some(RuntimeEffect::SetCargoProductionOverride {
|
||||
target: RuntimeCargoProductionTarget::Named { name },
|
||||
value,
|
||||
}))
|
||||
}
|
||||
230..=240 => {
|
||||
let Some(slot) = row.descriptor_id.checked_sub(229) else {
|
||||
return Ok(None);
|
||||
|
|
@ -2650,6 +2661,9 @@ fn company_target_import_error_message(
|
|||
Some(ImportBlocker::NamedTerritoryBinding) => {
|
||||
"packed condition requires named territory binding".to_string()
|
||||
}
|
||||
Some(ImportBlocker::EvidenceBlockedDescriptor) => {
|
||||
"packed descriptor is still evidence-blocked".to_string()
|
||||
}
|
||||
Some(ImportBlocker::UnmappedOrdinaryCondition) => {
|
||||
"packed ordinary condition is not yet mapped".to_string()
|
||||
}
|
||||
|
|
@ -3107,6 +3121,7 @@ fn company_target_import_outcome(blocker: ImportBlocker) -> &'static str {
|
|||
ImportBlocker::NamedTerritoryBinding => "blocked_named_territory_binding",
|
||||
ImportBlocker::UnmappedOrdinaryCondition => "blocked_unmapped_ordinary_condition",
|
||||
ImportBlocker::UnmappedWorldCondition => "blocked_unmapped_world_condition",
|
||||
ImportBlocker::EvidenceBlockedDescriptor => "blocked_evidence_blocked_descriptor",
|
||||
ImportBlocker::MissingTrainContext => "blocked_missing_train_context",
|
||||
ImportBlocker::MissingTrainTerritoryContext => "blocked_missing_train_territory_context",
|
||||
ImportBlocker::MissingLocomotiveCatalogContext => {
|
||||
|
|
@ -3832,6 +3847,7 @@ mod tests {
|
|||
semantic_preview: Some("Set Company Cash to 7 with aux [2, 3, 24, 36]".to_string()),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: Some("Mikado".to_string()),
|
||||
notes: vec!["grouped effect row carries locomotive-name side string".to_string()],
|
||||
|
|
@ -3866,6 +3882,7 @@ mod tests {
|
|||
)),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -3895,6 +3912,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -3924,6 +3942,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Credit Rating to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -3955,6 +3974,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Merger Premium to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![
|
||||
|
|
@ -3992,6 +4012,7 @@ mod tests {
|
|||
)),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4027,6 +4048,7 @@ mod tests {
|
|||
)),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes,
|
||||
|
|
@ -4056,6 +4078,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Economic Status to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4087,6 +4110,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Limited Track Building Amount to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4118,6 +4142,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Use Wartime Cargos to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4149,6 +4174,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Turbo Diesel Availability to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4264,6 +4290,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set {descriptor_label} to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4374,6 +4401,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set {descriptor_label} to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4628,6 +4656,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set {descriptor_label} to {value}")),
|
||||
recovered_cargo_slot: Some(slot),
|
||||
recovered_cargo_class,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4657,6 +4686,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set All Cargo Prices to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![
|
||||
|
|
@ -4691,6 +4721,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Unknown Cargo Price to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![
|
||||
|
|
@ -4731,6 +4762,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set {label} to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![
|
||||
|
|
@ -4743,11 +4775,19 @@ mod tests {
|
|||
descriptor_id: u32,
|
||||
value: i32,
|
||||
) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
let cargo_label = match descriptor_id {
|
||||
180 => Some("Alcohol".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
let descriptor_label = cargo_label
|
||||
.as_ref()
|
||||
.map(|label| format!("{label} Production"))
|
||||
.unwrap_or_else(|| "Unknown Cargo Production".to_string());
|
||||
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
group_index: 0,
|
||||
row_index: 0,
|
||||
descriptor_id,
|
||||
descriptor_label: Some("Unknown Cargo Production".to_string()),
|
||||
descriptor_label: Some(descriptor_label.clone()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("cargo_production_scalar".to_string()),
|
||||
grouped_target_subject: None,
|
||||
|
|
@ -4762,9 +4802,10 @@ mod tests {
|
|||
value_word_0x16: 0,
|
||||
row_shape: "scalar_assignment".to_string(),
|
||||
semantic_family: Some("scalar_assignment".to_string()),
|
||||
semantic_preview: Some(format!("Set Unknown Cargo Production to {value}")),
|
||||
semantic_preview: Some(format!("Set {descriptor_label} to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: cargo_label,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![
|
||||
|
|
@ -4798,6 +4839,7 @@ mod tests {
|
|||
semantic_preview: Some(format!("Set Territory Access Cost to {value}")),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4834,6 +4876,7 @@ mod tests {
|
|||
)),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4868,6 +4911,7 @@ mod tests {
|
|||
)),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -4904,6 +4948,7 @@ mod tests {
|
|||
)),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: locomotive_name.map(ToString::to_string),
|
||||
notes,
|
||||
|
|
@ -4933,6 +4978,7 @@ mod tests {
|
|||
semantic_preview: Some("Set Confiscate All to FALSE".to_string()),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -6952,6 +6998,7 @@ mod tests {
|
|||
),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: Some(10),
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
|
|
@ -8066,7 +8113,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_named_cargo_production_rows_evidence_blocked() {
|
||||
fn imports_named_cargo_production_rows_when_binding_is_grounded() {
|
||||
let save_slice = SmpLoadedSaveSlice {
|
||||
file_extension_hint: Some("gms".to_string()),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
|
|
@ -8096,13 +8143,13 @@ mod tests {
|
|||
live_record_count: 1,
|
||||
live_entry_ids: vec![40],
|
||||
decoded_record_count: 1,
|
||||
imported_runtime_record_count: 0,
|
||||
imported_runtime_record_count: 1,
|
||||
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||
record_index: 0,
|
||||
live_entry_id: 40,
|
||||
payload_offset: Some(0x7202),
|
||||
payload_len: Some(96),
|
||||
decode_status: "parity_only".to_string(),
|
||||
decode_status: "executable".to_string(),
|
||||
payload_family: "real_packed_v1".to_string(),
|
||||
trigger_kind: Some(7),
|
||||
active: None,
|
||||
|
|
@ -8116,30 +8163,45 @@ mod tests {
|
|||
grouped_effect_row_counts: vec![1, 0, 0, 0],
|
||||
grouped_effect_rows: vec![real_named_cargo_production_row(180, 160)],
|
||||
decoded_conditions: Vec::new(),
|
||||
decoded_actions: vec![],
|
||||
executable_import_ready: false,
|
||||
notes: vec!["named cargo production descriptors remain evidence-blocked until cargo ordering is pinned"
|
||||
decoded_actions: vec![RuntimeEffect::SetCargoProductionOverride {
|
||||
target: RuntimeCargoProductionTarget::Named {
|
||||
name: "Alcohol".to_string(),
|
||||
},
|
||||
value: 160,
|
||||
}],
|
||||
executable_import_ready: true,
|
||||
notes: vec!["named cargo production descriptors now import through named cargo overrides"
|
||||
.to_string()],
|
||||
}],
|
||||
}),
|
||||
notes: vec![],
|
||||
};
|
||||
|
||||
let import = project_save_slice_to_runtime_state_import(
|
||||
let mut import = project_save_slice_to_runtime_state_import(
|
||||
&save_slice,
|
||||
"packed-events-named-cargo-production-parity",
|
||||
None,
|
||||
)
|
||||
.expect("save slice should project");
|
||||
|
||||
assert!(import.state.event_runtime_records.is_empty());
|
||||
assert_eq!(import.state.event_runtime_records.len(), 1);
|
||||
execute_step_command(
|
||||
&mut import.state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("named cargo production runtime record should run");
|
||||
|
||||
assert_eq!(
|
||||
import.state.named_cargo_production_overrides.get("Alcohol"),
|
||||
Some(&160)
|
||||
);
|
||||
assert_eq!(
|
||||
import
|
||||
.state
|
||||
.packed_event_collection
|
||||
.as_ref()
|
||||
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
|
||||
Some("blocked_evidence_blocked_descriptor")
|
||||
Some("imported")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -8451,6 +8513,7 @@ mod tests {
|
|||
),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: Some("Mikado".to_string()),
|
||||
notes: vec![
|
||||
|
|
@ -10080,6 +10143,7 @@ mod tests {
|
|||
semantic_preview: Some("Set Turbo Diesel Availability to 1".to_string()),
|
||||
recovered_cargo_slot: None,
|
||||
recovered_cargo_class: None,
|
||||
recovered_cargo_label: None,
|
||||
recovered_locomotive_id: None,
|
||||
locomotive_name: None,
|
||||
notes: vec!["checked-in whole-game grouped-effect sample".to_string()],
|
||||
|
|
|
|||
|
|
@ -2189,6 +2189,8 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary {
|
|||
#[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>,
|
||||
|
|
@ -3510,6 +3512,11 @@ fn parse_real_grouped_effect_row_summary(
|
|||
"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}"
|
||||
));
|
||||
}
|
||||
|
||||
Some(SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
group_index,
|
||||
|
|
@ -3543,6 +3550,8 @@ fn parse_real_grouped_effect_row_summary(
|
|||
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)
|
||||
.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,
|
||||
|
|
@ -3789,9 +3798,87 @@ fn recovered_cargo_economics_descriptor_metadata(
|
|||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
];
|
||||
|
||||
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,
|
||||
|
|
@ -11493,6 +11580,18 @@ mod tests {
|
|||
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 =
|
||||
|
|
@ -11556,6 +11655,33 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[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 looks_up_recovered_locomotive_policy_descriptor_metadata() {
|
||||
let metadata =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue