Ground named cargo production event descriptors

This commit is contained in:
Jan Petykiewicz 2026-04-16 23:44:55 -07:00
commit 6a5d028d19
14 changed files with 987 additions and 217 deletions

View file

@ -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);

View file

@ -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",
];

View file

@ -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()],

View file

@ -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 =