diff --git a/README.md b/README.md index 0e24950..250a882 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ still does not reconstruct those company/chairman collections automatically, but reconstruct selection-only company/chairman context from the fixed save-side `0x32c8` world block. Those raw selected ids can flow through save-slice export/import and override overlay-backed base selection even while the full raw rosters remain absent, and a tracked overlay fixture now pins -that selection-only override path explicitly. A checked-in +that selection-only override path explicitly. The same fixed block now also exports the grounded +campaign override byte plus the raw chairman slot selector and role-gate bytes as analysis-only +save fields. A checked-in `EventEffects` export now exists too in `artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now exists beside it in `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`. Recovered @@ -45,7 +47,8 @@ company-governance scalar effect surface: descriptor `56` `Credit Rating` and descriptor `57` `Prime Rate` execute from ordinary real packed rows, while adjacent recovered finance/control-transfer descriptors such as `55` `Stock Prices` and `58` `Merger Premium` now land on explicit shell-owned parity instead of anonymous unmapped -descriptor residue, and tracked shell-owned fixtures now pin both finance rows explicitly. The +descriptor residue, and tracked shell-owned fixtures now pin finance, scenario-outcome, and +control-transfer shell rows explicitly. The recovered whole-game scalar economy/performance strip `59..104` now has a bounded runtime landing surface too: representative descriptors import into `RuntimeState.world_scalar_overrides` through stable normalized keys such as @@ -68,8 +71,10 @@ offline cargo-source inspector now pushes that groundwork further in rehosted co `artifacts/exports/rt3-1.06/economy-cargo-sources.json` report parses both `CargoTypes` and the `Cargo106.PK4` `cargoSkin` descriptors, normalizes localized `~####Name` tokens into visible names, builds a merged live cargo registry, and derives an exact named cargo-production selector -from the checked-in bindings. It also shows that the current 1.06 visible-name union is `80`, not -`71`, so source recovery alone still does not prove the live price-selector ordering. The +from the checked-in bindings. Dedicated CLI inspector commands now expose that production selector +and the unresolved price-selector candidate registry directly. The same report still shows that the +current 1.06 visible-name union is `80`, not `71`, so source recovery alone still does not prove +the live price-selector ordering. The add-building strip `503..519` is now explicitly classified as recovered shell-owned descriptor parity rather than generic unresolved residue. The first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and diff --git a/artifacts/exports/rt3-1.06/economy-cargo-sources.json b/artifacts/exports/rt3-1.06/economy-cargo-sources.json index 95f6100..5e72d26 100644 --- a/artifacts/exports/rt3-1.06/economy-cargo-sources.json +++ b/artifacts/exports/rt3-1.06/economy-cargo-sources.json @@ -972,6 +972,39 @@ ] } ], + "price_selector_candidate_excess_count": 9, + "price_selector_candidate_only_visible_names": [ + "Beer", + "Candidates", + "China", + "Containers", + "Detergents", + "Deuterium", + "Energy", + "Fish", + "Food", + "Glass", + "Gravel", + "Money", + "Newspaper", + "Paint", + "Perfume", + "Potash", + "Pottery", + "Prisoners", + "Rock", + "Salt", + "Sand", + "Spaceships", + "Syrup", + "Tea", + "Tin", + "Tobacco", + "Tools", + "Valuables", + "Wine", + "Wire" + ], "production_selector": { "selector_kind": "named_cargo_production", "exact_resolution": true, diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index 0809a46..7b96f62 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -18,16 +18,17 @@ use rrt_model::{ }; use rrt_runtime::{ CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, CargoEconomySourceReport, - CargoSkinInspectionReport, CargoTypeInspectionReport, OBSERVED_CAMPAIGN_SCENARIO_NAMES, - OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, Pk4ExtractionReport, Pk4InspectionReport, - RuntimeOverlayImportDocument, RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, - RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, RuntimeSnapshotSource, RuntimeSummary, - SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock, - SmpInspectionReport, SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary, - WinInspectionReport, execute_step_command, extract_pk4_entry_file, inspect_campaign_exe_file, - inspect_cargo_economy_sources_with_bindings, inspect_cargo_skin_pk4, inspect_cargo_types_dir, - inspect_pk4_file, inspect_smp_file, inspect_win_file, load_runtime_snapshot_document, - load_runtime_state_import, load_save_slice_file, project_save_slice_to_runtime_state_import, + CargoSelectorReport, CargoSkinInspectionReport, CargoTypeInspectionReport, + OBSERVED_CAMPAIGN_SCENARIO_NAMES, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, Pk4ExtractionReport, + Pk4InspectionReport, RuntimeOverlayImportDocument, RuntimeOverlayImportDocumentSource, + RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, + RuntimeSnapshotSource, RuntimeSummary, SAVE_SLICE_DOCUMENT_FORMAT_VERSION, + SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock, SmpInspectionReport, SmpLoadedSaveSlice, + SmpRt3105PackedProfileBlock, SmpSaveLoadSummary, WinInspectionReport, execute_step_command, + extract_pk4_entry_file, inspect_campaign_exe_file, inspect_cargo_economy_sources_with_bindings, + inspect_cargo_skin_pk4, inspect_cargo_types_dir, inspect_pk4_file, inspect_smp_file, + inspect_win_file, load_runtime_snapshot_document, load_runtime_state_import, + load_save_slice_file, project_save_slice_to_runtime_state_import, save_runtime_overlay_import_document, save_runtime_save_slice_document, save_runtime_snapshot_document, validate_runtime_snapshot_document, }; @@ -151,6 +152,14 @@ enum Command { cargo_types_dir: PathBuf, cargo_skin_pk4_path: PathBuf, }, + RuntimeInspectCargoProductionSelector { + cargo_types_dir: PathBuf, + cargo_skin_pk4_path: PathBuf, + }, + RuntimeInspectCargoPriceSelector { + cargo_types_dir: PathBuf, + cargo_skin_pk4_path: PathBuf, + }, RuntimeInspectWin { win_path: PathBuf, }, @@ -303,6 +312,13 @@ struct RuntimeCargoEconomyInspectionOutput { inspection: CargoEconomySourceReport, } +#[derive(Debug, Serialize)] +struct RuntimeCargoSelectorInspectionOutput { + cargo_types_dir: String, + cargo_skin_pk4_path: String, + selector: CargoSelectorReport, +} + #[derive(Debug, Serialize)] struct RuntimeWinInspectionOutput { path: String, @@ -860,6 +876,18 @@ fn real_main() -> Result<(), Box> { } => { run_runtime_inspect_cargo_economy_sources(&cargo_types_dir, &cargo_skin_pk4_path)?; } + Command::RuntimeInspectCargoProductionSelector { + cargo_types_dir, + cargo_skin_pk4_path, + } => { + run_runtime_inspect_cargo_production_selector(&cargo_types_dir, &cargo_skin_pk4_path)?; + } + Command::RuntimeInspectCargoPriceSelector { + cargo_types_dir, + cargo_skin_pk4_path, + } => { + run_runtime_inspect_cargo_price_selector(&cargo_types_dir, &cargo_skin_pk4_path)?; + } Command::RuntimeInspectWin { win_path } => { run_runtime_inspect_win(&win_path)?; } @@ -1060,6 +1088,22 @@ fn parse_command() -> Result> { cargo_skin_pk4_path: PathBuf::from(cargo_skin_pk4_path), }) } + [command, subcommand, cargo_types_dir, cargo_skin_pk4_path] + if command == "runtime" && subcommand == "inspect-cargo-production-selector" => + { + Ok(Command::RuntimeInspectCargoProductionSelector { + cargo_types_dir: PathBuf::from(cargo_types_dir), + cargo_skin_pk4_path: PathBuf::from(cargo_skin_pk4_path), + }) + } + [command, subcommand, cargo_types_dir, cargo_skin_pk4_path] + if command == "runtime" && subcommand == "inspect-cargo-price-selector" => + { + Ok(Command::RuntimeInspectCargoPriceSelector { + cargo_types_dir: PathBuf::from(cargo_types_dir), + cargo_skin_pk4_path: PathBuf::from(cargo_skin_pk4_path), + }) + } [command, subcommand, path] if command == "runtime" && subcommand == "inspect-win" => { Ok(Command::RuntimeInspectWin { win_path: PathBuf::from(path), @@ -1195,7 +1239,7 @@ fn parse_command() -> Result> { }) } _ => Err( - "usage: rrt-cli [validate [repo-root] | finance eval | finance diff | runtime validate-fixture | runtime summarize-fixture | runtime export-fixture-state | runtime diff-state | runtime summarize-state | runtime import-state | runtime inspect-smp | runtime summarize-save-load | runtime load-save-slice | runtime import-save-state | runtime export-save-slice | runtime export-overlay-import | runtime inspect-pk4 | runtime inspect-cargo-types | runtime inspect-cargo-skins | runtime inspect-cargo-economy-sources | runtime inspect-win | runtime extract-pk4-entry | runtime inspect-campaign-exe | runtime compare-classic-profile [saveN.gms...] | runtime compare-105-profile [saveN.gms...] | runtime compare-candidate-table [fileN...] | runtime compare-recipe-book-lines [fileN...] | runtime compare-setup-payload-core [fileN...] | runtime compare-setup-launch-payload [fileN...] | runtime compare-post-special-conditions-scalars [fileN...] | runtime scan-candidate-table-headers | runtime scan-special-conditions | runtime scan-aligned-runtime-rule-band | runtime scan-post-special-conditions-scalars | runtime scan-post-special-conditions-tail | runtime scan-recipe-book-lines | runtime export-profile-block ]" + "usage: rrt-cli [validate [repo-root] | finance eval | finance diff | runtime validate-fixture | runtime summarize-fixture | runtime export-fixture-state | runtime diff-state | runtime summarize-state | runtime import-state | runtime inspect-smp | runtime summarize-save-load | runtime load-save-slice | runtime import-save-state | runtime export-save-slice | runtime export-overlay-import | runtime inspect-pk4 | runtime inspect-cargo-types | runtime inspect-cargo-skins | runtime inspect-cargo-economy-sources | runtime inspect-cargo-production-selector | runtime inspect-cargo-price-selector | runtime inspect-win | runtime extract-pk4-entry | runtime inspect-campaign-exe | runtime compare-classic-profile [saveN.gms...] | runtime compare-105-profile [saveN.gms...] | runtime compare-candidate-table [fileN...] | runtime compare-recipe-book-lines [fileN...] | runtime compare-setup-payload-core [fileN...] | runtime compare-setup-launch-payload [fileN...] | runtime compare-post-special-conditions-scalars [fileN...] | runtime scan-candidate-table-headers | runtime scan-special-conditions | runtime scan-aligned-runtime-rule-band | runtime scan-post-special-conditions-scalars | runtime scan-post-special-conditions-tail | runtime scan-recipe-book-lines | runtime export-profile-block ]" .into(), ), } @@ -1598,6 +1642,49 @@ fn run_runtime_inspect_cargo_economy_sources( Ok(()) } +fn run_runtime_inspect_cargo_production_selector( + cargo_types_dir: &Path, + cargo_skin_pk4_path: &Path, +) -> Result<(), Box> { + let cargo_bindings_path = + Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json"); + let inspection = inspect_cargo_economy_sources_with_bindings( + cargo_types_dir, + cargo_skin_pk4_path, + Some(cargo_bindings_path), + )?; + let selector = inspection + .production_selector + .ok_or("named cargo production selector is not available in the checked-in bindings")?; + let report = RuntimeCargoSelectorInspectionOutput { + cargo_types_dir: cargo_types_dir.display().to_string(), + cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(), + selector, + }; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} + +fn run_runtime_inspect_cargo_price_selector( + cargo_types_dir: &Path, + cargo_skin_pk4_path: &Path, +) -> Result<(), Box> { + let cargo_bindings_path = + Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json"); + let inspection = inspect_cargo_economy_sources_with_bindings( + cargo_types_dir, + cargo_skin_pk4_path, + Some(cargo_bindings_path), + )?; + let report = RuntimeCargoSelectorInspectionOutput { + cargo_types_dir: cargo_types_dir.display().to_string(), + cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(), + selector: inspection.price_selector, + }; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} + fn run_runtime_inspect_win(win_path: &Path) -> Result<(), Box> { let report = RuntimeWinInspectionOutput { path: win_path.display().to_string(), @@ -4665,9 +4752,14 @@ mod tests { ); let stock_prices_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../fixtures/runtime/packed-event-stock-prices-shell-save-slice-fixture.json"); + let game_won_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/runtime/packed-event-game-won-shell-save-slice-fixture.json"); let merger_premium_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "../../fixtures/runtime/packed-event-merger-premium-shell-save-slice-fixture.json", ); + let set_human_control_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../fixtures/runtime/packed-event-set-human-control-shell-save-slice-fixture.json", + ); let investor_confidence_condition_save_fixture = PathBuf::from(env!( "CARGO_MANIFEST_DIR" )) @@ -4771,8 +4863,12 @@ mod tests { .expect("save-slice-backed credit-rating descriptor fixture should summarize"); run_runtime_summarize_fixture(&stock_prices_shell_save_fixture) .expect("save-slice-backed shell-owned stock-prices fixture should summarize"); + run_runtime_summarize_fixture(&game_won_shell_save_fixture) + .expect("save-slice-backed shell-owned game-won fixture should summarize"); run_runtime_summarize_fixture(&merger_premium_shell_save_fixture) .expect("save-slice-backed shell-owned merger-premium fixture should summarize"); + run_runtime_summarize_fixture(&set_human_control_shell_save_fixture) + .expect("save-slice-backed shell-owned set-human-control fixture should summarize"); run_runtime_summarize_fixture(&investor_confidence_condition_save_fixture) .expect("save-slice-backed investor-confidence condition fixture should summarize"); run_runtime_summarize_fixture(&management_attitude_condition_save_fixture) diff --git a/crates/rrt-runtime/src/economy.rs b/crates/rrt-runtime/src/economy.rs index 0acfdaa..6e22eb2 100644 --- a/crates/rrt-runtime/src/economy.rs +++ b/crates/rrt-runtime/src/economy.rs @@ -69,6 +69,8 @@ pub struct CargoEconomySourceReport { pub cargo_skin_only_visible_names: Vec, pub live_registry_count: usize, pub live_registry_entries: Vec, + pub price_selector_candidate_excess_count: usize, + pub price_selector_candidate_only_visible_names: Vec, pub production_selector: Option, pub price_selector: CargoSelectorReport, pub notes: Vec, @@ -271,7 +273,25 @@ fn build_cargo_economy_source_report( build_live_registry_entries(&cargo_types.entries, &cargo_skins.entries); let production_selector = cargo_bindings.map(|bindings| build_production_selector(bindings, &live_registry_entries)); + let price_selector_candidate_only_visible_names = production_selector + .as_ref() + .map(|selector| { + let selector_names = selector + .entries + .iter() + .map(|entry| entry.visible_name.as_str()) + .collect::>(); + live_registry_entries + .iter() + .filter(|entry| !selector_names.contains(entry.visible_name.as_str())) + .map(|entry| entry.visible_name.clone()) + .collect::>() + }) + .unwrap_or_default(); let price_selector = build_price_selector(&live_registry_entries); + let price_selector_candidate_excess_count = live_registry_entries + .len() + .saturating_sub(NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT); let mut notes = Vec::new(); notes.push(format!( @@ -313,6 +333,8 @@ fn build_cargo_economy_source_report( cargo_skin_only_visible_names, live_registry_count: live_registry_entries.len(), live_registry_entries, + price_selector_candidate_excess_count, + price_selector_candidate_only_visible_names, production_selector, price_selector, notes, @@ -661,6 +683,12 @@ mod tests { ); assert!(!report.price_selector.exact_resolution); assert_eq!(report.price_selector.candidate_registry_count, 3); + assert_eq!(report.price_selector_candidate_excess_count, 0); + assert!( + report + .price_selector_candidate_only_visible_names + .is_empty() + ); assert!(report.production_selector.is_none()); } @@ -733,5 +761,11 @@ mod tests { ] ); assert_eq!(selector.entries[1].visible_name, "Coal"); + assert!( + report + .price_selector_candidate_only_visible_names + .is_empty() + ); + assert_eq!(report.price_selector_candidate_excess_count, 0); } } diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 9f2e380..678b2a8 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -95,6 +95,11 @@ const RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG: u32 = 0x000032c9; const RT3_SAVE_WORLD_BLOCK_LEN: usize = 0x4f2c; const RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET: usize = 0x1d; const RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET: usize = 0x21; +const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET: usize = 0x83; +const RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET: usize = 0xc1; +const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET: usize = 0x0bbf; +const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT: usize = 16; +const RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE: usize = 9; const EVENT_RUNTIME_COLLECTION_METADATA_TAG: u16 = 0x4e99; const EVENT_RUNTIME_COLLECTION_RECORDS_TAG: u16 = 0x4e9a; const EVENT_RUNTIME_COLLECTION_CLOSE_TAG: u16 = 0x4e9b; @@ -1464,6 +1469,13 @@ pub struct SmpSaveWorldSelectionContextProbe { pub selected_chairman_profile_id_offset: usize, pub selected_chairman_profile_id: u32, pub selected_chairman_profile_id_hex: String, + pub chairman_slot_selector_offset: usize, + pub chairman_slot_selectors: Vec, + pub campaign_override_flag_offset: usize, + pub campaign_override_flag: u8, + pub campaign_override_flag_hex: String, + pub chairman_role_gate_offset: usize, + pub chairman_role_gate_bytes: Vec, pub evidence: Vec, } @@ -2551,6 +2563,13 @@ pub fn load_save_slice_from_report( "Raw save fixed world block exposes selected_chairman_profile_id={} at file offset 0x{:x}.", probe.selected_chairman_profile_id, probe.selected_chairman_profile_id_offset )); + notes.push(format!( + "Raw save fixed world block also exposes {} chairman slot selector bytes at file offset 0x{:x} and campaign_override_flag={} at file offset 0x{:x}.", + probe.chairman_slot_selectors.len(), + probe.chairman_slot_selector_offset, + probe.campaign_override_flag, + probe.campaign_override_flag_offset + )); notes.push( "Raw save inspection still does not reconstruct full company_roster or chairman_profile_table payloads; the grounded package-save path only proves selection ids and header-level collection state for those families." .to_string(), @@ -6933,8 +6952,31 @@ fn parse_save_world_selection_context_probe( payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_COMPANY_ID_RELATIVE_OFFSET; let selected_chairman_profile_id_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET; + let chairman_slot_selector_offset = + payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET; + let campaign_override_flag_offset = + payload_offset + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET; + let chairman_role_gate_offset = + payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET; let selected_company_id = read_u32_at(bytes, selected_company_id_offset)?; let selected_chairman_profile_id = read_u32_at(bytes, selected_chairman_profile_id_offset)?; + let chairman_slot_selectors = bytes + .get( + chairman_slot_selector_offset + ..chairman_slot_selector_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT, + )? + .to_vec(); + let campaign_override_flag = *bytes.get(campaign_override_flag_offset)?; + let chairman_role_gate_bytes = (0..RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT) + .map(|slot_index| { + bytes + .get( + chairman_role_gate_offset + + slot_index * RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE, + ) + .copied() + }) + .collect::>>()?; return Some(SmpSaveWorldSelectionContextProbe { profile_family: profile.profile_family.clone(), source_kind: "save-direct-world-block".to_string(), @@ -6949,6 +6991,13 @@ fn parse_save_world_selection_context_probe( selected_chairman_profile_id_offset, selected_chairman_profile_id, selected_chairman_profile_id_hex: format!("0x{selected_chairman_profile_id:08x}"), + chairman_slot_selector_offset, + chairman_slot_selectors, + campaign_override_flag_offset, + campaign_override_flag, + campaign_override_flag_hex: format!("0x{campaign_override_flag:02x}"), + chairman_role_gate_offset, + chairman_role_gate_bytes, evidence: vec![ format!( "chunk tag 0x32c8 at 0x{chunk_tag_offset:x} matches the fixed [world+0x04] save block" @@ -6964,6 +7013,19 @@ fn parse_save_world_selection_context_probe( "selected chairman profile id comes from payload +0x{:x} ([world+0x25])", RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET ), + format!( + "16 chairman slot selector bytes come from payload +0x{:x} ([world+0x87])", + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET + ), + format!( + "campaign override flag comes from payload +0x{:x} ([world+0xc5])", + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET + ), + format!( + "chairman role-gate bytes come from payload +0x{:x} + slot*0x{:x} ([world+0x0bc3+slot*9])", + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET, + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE + ), ], }); } @@ -13248,6 +13310,17 @@ mod tests { + RT3_SAVE_WORLD_BLOCK_SELECTED_CHAIRMAN_PROFILE_ID_RELATIVE_OFFSET + 4] .copy_from_slice(&9u32.to_le_bytes()); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET + ..payload_offset + + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_SELECTOR_RELATIVE_OFFSET + + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_SLOT_COUNT] + .copy_from_slice(&[3, 1, 4, 1, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_CAMPAIGN_OVERRIDE_FLAG_RELATIVE_OFFSET] = 1; + for (slot_index, role_gate) in [2u8, 1, 0, 2].into_iter().enumerate() { + bytes[payload_offset + + RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_RELATIVE_OFFSET + + slot_index * RT3_SAVE_WORLD_BLOCK_CHAIRMAN_ROLE_GATE_STRIDE] = role_gate; + } let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; bytes[next_chunk_offset..next_chunk_offset + 4] .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); @@ -13267,6 +13340,9 @@ mod tests { assert_eq!(probe.payload_offset, payload_offset); assert_eq!(probe.selected_company_id, 7); assert_eq!(probe.selected_chairman_profile_id, 9); + assert_eq!(probe.chairman_slot_selectors[..6], [3, 1, 4, 1, 5, 9]); + assert_eq!(probe.campaign_override_flag, 1); + assert_eq!(probe.chairman_role_gate_bytes[..4], [2, 1, 0, 2]); } #[test] @@ -13310,6 +13386,13 @@ mod tests { selected_chairman_profile_id_offset: 0x3f3, selected_chairman_profile_id: 9, selected_chairman_profile_id_hex: "0x00000009".to_string(), + chairman_slot_selector_offset: 0x455, + chairman_slot_selectors: vec![3, 1, 4, 1, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + campaign_override_flag_offset: 0x493, + campaign_override_flag: 1, + campaign_override_flag_hex: "0x01".to_string(), + chairman_role_gate_offset: 0xf91, + chairman_role_gate_bytes: vec![2, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], evidence: vec![], }); @@ -13339,6 +13422,12 @@ mod tests { .iter() .any(|note| note.contains("selected_chairman_profile_id=9")) ); + assert!( + slice + .notes + .iter() + .any(|note| note.contains("campaign_override_flag=1")) + ); } #[test] diff --git a/docs/README.md b/docs/README.md index 2a3bb7b..ef0f119 100644 --- a/docs/README.md +++ b/docs/README.md @@ -100,7 +100,8 @@ The highest-value next passes are now: but it now does reconstruct selection-only company/chairman context from the fixed save-side `0x32c8` world block, so overlay imports can reuse base rosters while honoring raw save-native selected company/chairman ids, and a tracked overlay fixture now pins that selection-only - override path explicitly + override path explicitly; the same fixed block now also exports the grounded campaign override + byte plus the raw chairman slot selector and role-gate bytes as analysis-only save fields - a checked-in `EventEffects` export now exists at `artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json` @@ -112,7 +113,8 @@ The highest-value next passes are now: `Prime Rate` - adjacent recovered finance/control-transfer descriptors such as `55` `Stock Prices` and `58` `Merger Premium` now land on explicit shell-owned descriptor parity instead of generic unmapped - descriptor residue, with tracked fixtures now pinning both finance rows explicitly + descriptor residue, with tracked fixtures now pinning finance, scenario-outcome, and + control-transfer shell rows explicitly - the recovered whole-game scalar economy/performance strip `59..104` now has a bounded runtime landing surface too: representative rows execute into `RuntimeState.world_scalar_overrides` through stable normalized keys such as `world.build_stations_cost` and @@ -135,9 +137,10 @@ The highest-value next passes are now: `artifacts/exports/rt3-1.06/economy-cargo-sources.json` now parses both `CargoTypes` and the `Cargo106.PK4` `cargoSkin` descriptors through rehosted code, normalizes localized `~####Name` tokens into visible names, builds a merged live cargo registry, and derives an exact - named cargo-production selector from the checked-in bindings; it also shows that the current - 1.06 visible-name union is `80`, so source recovery alone still does not prove the live - price-selector ordering + named cargo-production selector from the checked-in bindings; dedicated CLI inspector commands + now expose that production selector and the unresolved price-selector candidate registry + directly, and the same report still shows that the current 1.06 visible-name union is `80`, so + source recovery alone still does not prove the live price-selector ordering - the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity, with tracked fixture coverage, instead of generic unresolved descriptor residue - widen real packed-event executable coverage descriptor by descriptor after identity, target mask, diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 02d9946..efe63e6 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -61,8 +61,10 @@ Implemented today: without overlay snapshots when the checked-in documents include that context, while raw `.gms` inspection/export still leaves full company/chairman rosters absent; the grounded raw-save tranche now covers only selection-only company/chairman context from the fixed `0x32c8` world - block, which overlay import can use to replace selected ids while preserving base rosters; a - tracked overlay fixture now pins that selection-only override path explicitly + block, which overlay import can use to replace selected ids while preserving base rosters; that + same fixed block now also exports the grounded campaign override byte plus the raw chairman slot + selector and role-gate bytes as analysis-only fields, and a tracked overlay fixture now pins the + selection-only override path explicitly - a checked-in `EventEffects` export now exists too at `artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json` @@ -74,7 +76,8 @@ Implemented today: `Prime Rate` now import through ordinary company target lowering - adjacent recovered finance/control-transfer descriptors such as `55` `Stock Prices` and `58` `Merger Premium` now land on explicit shell-owned descriptor parity instead of generic unmapped - descriptor buckets, with tracked fixtures now pinning both finance rows explicitly + descriptor buckets, with tracked fixtures now pinning finance, scenario-outcome, and + control-transfer shell rows explicitly - the recovered whole-game scalar economy/performance strip `59..104` now has a bounded runtime landing surface too: representative descriptors import as `SetWorldScalarOverride` and land in `RuntimeState.world_scalar_overrides` @@ -96,9 +99,10 @@ Implemented today: `artifacts/exports/rt3-1.06/economy-cargo-sources.json` now parses both `CargoTypes` and the `Cargo106.PK4` `cargoSkin` descriptors through rehosted code, normalizes localized `~####Name` tokens into visible names, builds a merged live cargo registry, and derives an exact - named cargo-production selector from the checked-in bindings; it also shows that the current - 1.06 visible-name union is `80`, so source recovery alone still does not prove the live - price-selector ordering + named cargo-production selector from the checked-in bindings; dedicated CLI inspector commands + now expose that production selector and the unresolved price-selector candidate registry + directly, and the same report still shows that the current 1.06 visible-name union is `80`, so + source recovery alone still does not prove the live price-selector ordering - the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity with tracked fixture coverage, not generic unresolved descriptor residue - a minimal event-owned train surface and an opaque economic-status lane now exist in runtime diff --git a/fixtures/runtime/packed-event-game-won-shell-save-slice-fixture.json b/fixtures/runtime/packed-event-game-won-shell-save-slice-fixture.json new file mode 100644 index 0000000..f69b93c --- /dev/null +++ b/fixtures/runtime/packed-event-game-won-shell-save-slice-fixture.json @@ -0,0 +1,37 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-game-won-shell-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture pinning the explicit shell-owned descriptor frontier for recovered Game Won rows." + }, + "state_save_slice_path": "packed-event-game-won-shell-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + } + ], + "expected_summary": { + "calendar_projection_source": "default-1830-placeholder", + "calendar_projection_is_placeholder": true, + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_parity_only_record_count": 1, + "packed_event_blocked_shell_owned_descriptor_count": 1, + "event_runtime_record_count": 0, + "total_event_record_service_count": 0, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "packed_event_collection": { + "records": [ + { + "import_outcome": "blocked_shell_owned_descriptor" + } + ] + }, + "event_runtime_records": [] + } +} diff --git a/fixtures/runtime/packed-event-game-won-shell-save-slice.json b/fixtures/runtime/packed-event-game-won-shell-save-slice.json new file mode 100644 index 0000000..b5e5297 --- /dev/null +++ b/fixtures/runtime/packed-event-game-won-shell-save-slice.json @@ -0,0 +1,110 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-game-won-shell-save-slice", + "source": { + "description": "Tracked save-slice document pinning a recovered shell-owned scenario outcome descriptor.", + "original_save_filename": "captured-game-won-shell.gms", + "original_save_sha256": "game-won-shell-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "pins descriptor 4 as explicit shell-owned parity instead of generic descriptor residue" + ] + }, + "save_slice": { + "file_extension_hint": "gms", + "container_profile_family": "rt3-classic-save-container-v1", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "trailer_family": null, + "bridge_family": null, + "profile": null, + "candidate_availability_table": null, + "named_locomotive_availability_table": null, + "locomotive_catalog": null, + "cargo_catalog": null, + "company_roster": null, + "chairman_profile_table": null, + "special_conditions_table": null, + "event_runtime_collection": { + "source_kind": "packed-event-runtime-collection", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "container_profile_family": "rt3-classic-save-container-v1", + "metadata_tag_offset": 28800, + "records_tag_offset": 29056, + "close_tag_offset": 29568, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 75, + "live_record_count": 1, + "live_entry_ids": [75], + "decoded_record_count": 1, + "imported_runtime_record_count": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 75, + "payload_offset": 29058, + "payload_len": 120, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "active": null, + "marks_collection_dirty": null, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 1, + "modifier_flag_0x7f9": 0, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [0, 0, 0, 0], + "grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "negative_sentinel_scope": null, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 4, + "descriptor_label": "Game Won (Bronze)", + "target_mask_bits": 2, + "parameter_family": "scenario_outcome_shell_action", + "opcode": 3, + "raw_scalar_value": 1, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Game Won (Bronze) to 1", + "locomotive_name": null, + "notes": [ + "descriptor recovered in the checked-in effect table as shell_owned parity" + ] + } + ], + "decoded_conditions": [], + "decoded_actions": [], + "executable_import_ready": false, + "notes": [ + "scenario-outcome descriptor is recovered but remains shell-owned parity" + ] + } + ] + }, + "notes": [ + "recovered shell-owned scenario outcome descriptor sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-set-human-control-shell-save-slice-fixture.json b/fixtures/runtime/packed-event-set-human-control-shell-save-slice-fixture.json new file mode 100644 index 0000000..0aa2101 --- /dev/null +++ b/fixtures/runtime/packed-event-set-human-control-shell-save-slice-fixture.json @@ -0,0 +1,41 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-set-human-control-shell-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture pinning the explicit shell-owned descriptor frontier for recovered Set to human control rows." + }, + "state_save_slice_path": "packed-event-set-human-control-shell-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + } + ], + "expected_summary": { + "calendar_projection_source": "default-1830-placeholder", + "calendar_projection_is_placeholder": true, + "company_count": 1, + "chairman_profile_count": 1, + "selected_company_id": 1, + "selected_chairman_profile_id": 1, + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_parity_only_record_count": 1, + "packed_event_blocked_shell_owned_descriptor_count": 1, + "event_runtime_record_count": 0, + "total_event_record_service_count": 0, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "packed_event_collection": { + "records": [ + { + "import_outcome": "blocked_shell_owned_descriptor" + } + ] + }, + "event_runtime_records": [] + } +} diff --git a/fixtures/runtime/packed-event-set-human-control-shell-save-slice.json b/fixtures/runtime/packed-event-set-human-control-shell-save-slice.json new file mode 100644 index 0000000..f4b980b --- /dev/null +++ b/fixtures/runtime/packed-event-set-human-control-shell-save-slice.json @@ -0,0 +1,161 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-set-human-control-shell-save-slice", + "source": { + "description": "Tracked save-slice document pinning a recovered shell-owned control-transfer descriptor.", + "original_save_filename": "captured-set-human-control-shell.gms", + "original_save_sha256": "set-human-control-shell-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "pins descriptor 24 as explicit shell-owned parity instead of generic descriptor residue" + ] + }, + "save_slice": { + "file_extension_hint": "gms", + "container_profile_family": "rt3-classic-save-container-v1", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "trailer_family": null, + "bridge_family": null, + "profile": null, + "candidate_availability_table": null, + "named_locomotive_availability_table": null, + "locomotive_catalog": null, + "cargo_catalog": null, + "special_conditions_table": null, + "event_runtime_collection": { + "source_kind": "packed-event-runtime-collection", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "container_profile_family": "rt3-classic-save-container-v1", + "metadata_tag_offset": 28896, + "records_tag_offset": 29152, + "close_tag_offset": 29664, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 75, + "live_record_count": 1, + "live_entry_ids": [75], + "decoded_record_count": 1, + "imported_runtime_record_count": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 75, + "payload_offset": 29154, + "payload_len": 120, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "active": null, + "marks_collection_dirty": null, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 1, + "modifier_flag_0x7f9": 1, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3], + "grouped_scope_checkboxes_0x7ff": [1, 0, 0, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "negative_sentinel_scope": null, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 24, + "descriptor_label": "Set to human control", + "target_mask_bits": 2, + "parameter_family": "control_transfer_shell_action", + "opcode": 3, + "raw_scalar_value": 1, + "value_byte_0x09": 0, + "value_dword_0x0d": 0, + "value_byte_0x11": 0, + "value_byte_0x12": 0, + "value_word_0x14": 0, + "value_word_0x16": 0, + "row_shape": "scalar_assignment", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Set to human control to 1", + "locomotive_name": null, + "notes": [ + "descriptor recovered in the checked-in effect table as shell_owned parity" + ] + } + ], + "decoded_conditions": [], + "decoded_actions": [], + "executable_import_ready": false, + "notes": [ + "control-transfer descriptor is recovered but remains shell-owned parity" + ] + } + ] + }, + "notes": [ + "recovered shell-owned control-transfer descriptor sample" + ], + "company_roster": { + "source_kind": "tracked-save-slice-company-roster", + "semantic_family": "save-slice-runtime-company-context", + "observed_entry_count": 1, + "selected_company_id": 1, + "entries": [ + { + "company_id": 1, + "active": true, + "controller_kind": "human", + "current_cash": 200, + "debt": 0, + "credit_rating_score": 700, + "prime_rate": 5, + "available_track_laying_capacity": 6, + "track_piece_counts": { + "total": 12, + "single": 4, + "double": 4, + "transition": 0, + "electric": 2, + "non_electric": 10 + }, + "linked_chairman_profile_id": 1, + "book_value_per_share": 1800, + "investor_confidence": 40, + "management_attitude": 45, + "takeover_cooldown_year": null, + "merger_cooldown_year": null + } + ] + }, + "chairman_profile_table": { + "source_kind": "tracked-save-slice-chairman-profile-table", + "semantic_family": "save-slice-runtime-chairman-context", + "observed_entry_count": 1, + "selected_chairman_profile_id": 1, + "entries": [ + { + "profile_id": 1, + "name": "Chairman One", + "active": true, + "current_cash": 500, + "linked_company_id": 1, + "company_holdings": { + "1": 1000 + }, + "holdings_value_total": 700, + "net_worth_total": 1200, + "purchasing_power_total": 1500 + } + ] + } + } +}