From 517b01cd354f95eb92a27f68c8a7afaaf5949917 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 Apr 2026 11:49:20 -0700 Subject: [PATCH] Rehost offline cargo economy sources --- README.md | 7 +- .../rt3-1.06/economy-cargo-sources.json | 1444 +++++++++++++++++ crates/rrt-cli/src/main.rs | 106 +- crates/rrt-model/src/lib.rs | 1 + crates/rrt-runtime/src/economy.rs | 417 +++++ crates/rrt-runtime/src/lib.rs | 7 + docs/README.md | 7 +- docs/runtime-rehost-plan.md | 7 +- 8 files changed, 1991 insertions(+), 5 deletions(-) create mode 100644 artifacts/exports/rt3-1.06/economy-cargo-sources.json create mode 100644 crates/rrt-runtime/src/economy.rs diff --git a/README.md b/README.md index cbb2e70..84b24f3 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,12 @@ remains explicit `blocked_evidence_blocked_descriptor` parity until descriptor o more strongly, but the semantic catalog now gives that band stable `Named Cargo Price Slot N` labels instead of anonymous `Unknown Cargo Price` residue. The checked-in static corpora now make that boundary more explicit too: the broader 1.06 CargoTypes corpus has `51` names and the 1.05 -corpus has `41`, so neither static set closes the `71`-row named price strip on its own. The +corpus has `41`, so neither static set closes the `71`-row named price strip on its own. A new +offline cargo-source inspector now pushes that groundwork further in rehosted code: the checked-in +`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, and 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 new file mode 100644 index 0000000..8b46807 --- /dev/null +++ b/artifacts/exports/rt3-1.06/economy-cargo-sources.json @@ -0,0 +1,1444 @@ +{ + "cargo_types_dir": "rt3_wineprefix/drive_c/rt3/Data/CargoTypes", + "cargo_skin_pk4_path": "rt3_wineprefix/drive_c/rt3/Data/PopTopExtraContent/Cargo106.PK4", + "named_cargo_price_row_count": 71, + "named_cargo_production_row_count": 50, + "cargo_type_count": 51, + "cargo_skin_count": 70, + "shared_visible_name_count": 41, + "visible_name_union_count": 80, + "cargo_type_only_visible_names": [ + "Ceramics", + "Concrete", + "Crystals", + "Dye", + "Electronics", + "Ingots", + "Machinery", + "Medicine", + "Ore", + "Rock" + ], + "cargo_skin_only_visible_names": [ + "Beer", + "Candidates", + "China", + "Containers", + "Detergents", + "Deuterium", + "Energy", + "Fish", + "Food", + "Glass", + "Gravel", + "Money", + "Newspaper", + "Paint", + "Perfume", + "Potash", + "Pottery", + "Prisoners", + "Salt", + "Sand", + "Spaceships", + "Syrup", + "Tea", + "Tin", + "Tobacco", + "Tools", + "Valuables", + "Wine", + "Wire" + ], + "notes": [ + "Named cargo-price descriptors 106..176 span 71 rows, while named cargo-production descriptors 180..229 span 50 rows.", + "The inspected CargoTypes corpus exposes 51 visible names, the inspected cargoSkin corpus exposes 70 visible names, and their union exposes 80 visible names.", + "That visible-name union still does not match the 71-row named cargo-price strip, so this offline source reconstruction is groundwork rather than a complete price-selector binding." + ], + "cargo_type_entries": [ + { + "file_name": "Alcohol.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Alcohol", + "visible_name": "Alcohol", + "localized_string_id": null + } + }, + { + "file_name": "Aluminum.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Aluminum", + "visible_name": "Aluminum", + "localized_string_id": null + } + }, + { + "file_name": "Ammunition.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Ammunition", + "visible_name": "Ammunition", + "localized_string_id": null + } + }, + { + "file_name": "Automobiles.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Automobiles", + "visible_name": "Automobiles", + "localized_string_id": null + } + }, + { + "file_name": "Bauxite.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Bauxite", + "visible_name": "Bauxite", + "localized_string_id": null + } + }, + { + "file_name": "~4465Ceramics.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4465Ceramics", + "visible_name": "Ceramics", + "localized_string_id": 4465 + } + }, + { + "file_name": "Cheese.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Cheese", + "visible_name": "Cheese", + "localized_string_id": null + } + }, + { + "file_name": "Chemicals.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Chemicals", + "visible_name": "Chemicals", + "localized_string_id": null + } + }, + { + "file_name": "Clothing.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Clothing", + "visible_name": "Clothing", + "localized_string_id": null + } + }, + { + "file_name": "Coal.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Coal", + "visible_name": "Coal", + "localized_string_id": null + } + }, + { + "file_name": "Coffee.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Coffee", + "visible_name": "Coffee", + "localized_string_id": null + } + }, + { + "file_name": "~4467Concrete.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4467Concrete", + "visible_name": "Concrete", + "localized_string_id": 4467 + } + }, + { + "file_name": "Corn.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Corn", + "visible_name": "Corn", + "localized_string_id": null + } + }, + { + "file_name": "Cotton.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Cotton", + "visible_name": "Cotton", + "localized_string_id": null + } + }, + { + "file_name": "~4469Crystals.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4469Crystals", + "visible_name": "Crystals", + "localized_string_id": 4469 + } + }, + { + "file_name": "Diesel.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Diesel", + "visible_name": "Diesel", + "localized_string_id": null + } + }, + { + "file_name": "~4475Dye.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4475Dye", + "visible_name": "Dye", + "localized_string_id": 4475 + } + }, + { + "file_name": "~4476Electronics.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4476Electronics", + "visible_name": "Electronics", + "localized_string_id": 4476 + } + }, + { + "file_name": "Fertilizer.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Fertilizer", + "visible_name": "Fertilizer", + "localized_string_id": null + } + }, + { + "file_name": "Furniture.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Furniture", + "visible_name": "Furniture", + "localized_string_id": null + } + }, + { + "file_name": "Goods.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Goods", + "visible_name": "Goods", + "localized_string_id": null + } + }, + { + "file_name": "Grain.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Grain", + "visible_name": "Grain", + "localized_string_id": null + } + }, + { + "file_name": "~4488Ingots.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4488Ingots", + "visible_name": "Ingots", + "localized_string_id": 4488 + } + }, + { + "file_name": "Iron.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Iron", + "visible_name": "Iron", + "localized_string_id": null + } + }, + { + "file_name": "Livestock.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Livestock", + "visible_name": "Livestock", + "localized_string_id": null + } + }, + { + "file_name": "Logs.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Logs", + "visible_name": "Logs", + "localized_string_id": null + } + }, + { + "file_name": "Lumber.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Lumber", + "visible_name": "Lumber", + "localized_string_id": null + } + }, + { + "file_name": "~4493Machinery.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4493Machinery", + "visible_name": "Machinery", + "localized_string_id": 4493 + } + }, + { + "file_name": "Mail.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Mail", + "visible_name": "Mail", + "localized_string_id": null + } + }, + { + "file_name": "Meat.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Meat", + "visible_name": "Meat", + "localized_string_id": null + } + }, + { + "file_name": "~4494Medicine.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4494Medicine", + "visible_name": "Medicine", + "localized_string_id": 4494 + } + }, + { + "file_name": "Milk.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Milk", + "visible_name": "Milk", + "localized_string_id": null + } + }, + { + "file_name": "Oil.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Oil", + "visible_name": "Oil", + "localized_string_id": null + } + }, + { + "file_name": "~4505Ore.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4505Ore", + "visible_name": "Ore", + "localized_string_id": 4505 + } + }, + { + "file_name": "Paper.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Paper", + "visible_name": "Paper", + "localized_string_id": null + } + }, + { + "file_name": "Passengers.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Passengers", + "visible_name": "Passengers", + "localized_string_id": null + } + }, + { + "file_name": "Plastic.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Plastic", + "visible_name": "Plastic", + "localized_string_id": null + } + }, + { + "file_name": "Produce.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Produce", + "visible_name": "Produce", + "localized_string_id": null + } + }, + { + "file_name": "Pulpwood.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Pulpwood", + "visible_name": "Pulpwood", + "localized_string_id": null + } + }, + { + "file_name": "Rice.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Rice", + "visible_name": "Rice", + "localized_string_id": null + } + }, + { + "file_name": "~4513Rock.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "~4513Rock", + "visible_name": "Rock", + "localized_string_id": 4513 + } + }, + { + "file_name": "Rubber.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Rubber", + "visible_name": "Rubber", + "localized_string_id": null + } + }, + { + "file_name": "Steel.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Steel", + "visible_name": "Steel", + "localized_string_id": null + } + }, + { + "file_name": "Sugar.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Sugar", + "visible_name": "Sugar", + "localized_string_id": null + } + }, + { + "file_name": "Tires.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Tires", + "visible_name": "Tires", + "localized_string_id": null + } + }, + { + "file_name": "Toys.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Toys", + "visible_name": "Toys", + "localized_string_id": null + } + }, + { + "file_name": "Troops.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Troops", + "visible_name": "Troops", + "localized_string_id": null + } + }, + { + "file_name": "Uranium.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Uranium", + "visible_name": "Uranium", + "localized_string_id": null + } + }, + { + "file_name": "Waste.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Waste", + "visible_name": "Waste", + "localized_string_id": null + } + }, + { + "file_name": "Weapons.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Weapons", + "visible_name": "Weapons", + "localized_string_id": null + } + }, + { + "file_name": "Wool.cty", + "file_size": 62, + "file_size_hex": "0x3e", + "header_magic": 1002, + "header_magic_hex": "0x000003ea", + "name": { + "raw_name": "Wool", + "visible_name": "Wool", + "localized_string_id": null + } + } + ], + "cargo_skin_entries": [ + { + "pk4_entry_name": "Alcohol.dsc", + "payload_len": 20, + "payload_len_hex": "0x14", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Alcohol", + "visible_name": "Alcohol", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Aluminum.dsc", + "payload_len": 21, + "payload_len_hex": "0x15", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Aluminum", + "visible_name": "Aluminum", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Ammunition.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Ammunition", + "visible_name": "Ammunition", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Automobiles.dsc", + "payload_len": 24, + "payload_len_hex": "0x18", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Automobiles", + "visible_name": "Automobiles", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Bauxite.dsc", + "payload_len": 20, + "payload_len_hex": "0x14", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Bauxite", + "visible_name": "Bauxite", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4459Beer.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4459Beer", + "visible_name": "Beer", + "localized_string_id": 4459 + } + }, + { + "pk4_entry_name": "~4464Candidates.dsc", + "payload_len": 28, + "payload_len_hex": "0x1c", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4464Candidates", + "visible_name": "Candidates", + "localized_string_id": 4464 + } + }, + { + "pk4_entry_name": "Cheese.dsc", + "payload_len": 19, + "payload_len_hex": "0x13", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Cheese", + "visible_name": "Cheese", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Chemicals.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Chemicals", + "visible_name": "Chemicals", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4466China.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4466China", + "visible_name": "China", + "localized_string_id": 4466 + } + }, + { + "pk4_entry_name": "Clothing.dsc", + "payload_len": 21, + "payload_len_hex": "0x15", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Clothing", + "visible_name": "Clothing", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Coal.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Coal", + "visible_name": "Coal", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Coffee.dsc", + "payload_len": 19, + "payload_len_hex": "0x13", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Coffee", + "visible_name": "Coffee", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4468Containers.dsc", + "payload_len": 28, + "payload_len_hex": "0x1c", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4468Containers", + "visible_name": "Containers", + "localized_string_id": 4468 + } + }, + { + "pk4_entry_name": "Corn.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Corn", + "visible_name": "Corn", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Cotton.dsc", + "payload_len": 19, + "payload_len_hex": "0x13", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Cotton", + "visible_name": "Cotton", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4471Detergents.dsc", + "payload_len": 28, + "payload_len_hex": "0x1c", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4471Detergents", + "visible_name": "Detergents", + "localized_string_id": 4471 + } + }, + { + "pk4_entry_name": "~4472Deuterium.dsc", + "payload_len": 27, + "payload_len_hex": "0x1b", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4472Deuterium", + "visible_name": "Deuterium", + "localized_string_id": 4472 + } + }, + { + "pk4_entry_name": "Diesel.dsc", + "payload_len": 19, + "payload_len_hex": "0x13", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Diesel", + "visible_name": "Diesel", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4477Energy.dsc", + "payload_len": 24, + "payload_len_hex": "0x18", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4477Energy", + "visible_name": "Energy", + "localized_string_id": 4477 + } + }, + { + "pk4_entry_name": "Fertilizer.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Fertilizer", + "visible_name": "Fertilizer", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4478Fish.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4478Fish", + "visible_name": "Fish", + "localized_string_id": 4478 + } + }, + { + "pk4_entry_name": "~4479Food.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4479Food", + "visible_name": "Food", + "localized_string_id": 4479 + } + }, + { + "pk4_entry_name": "Furniture.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Furniture", + "visible_name": "Furniture", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4481Glass.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4481Glass", + "visible_name": "Glass", + "localized_string_id": 4481 + } + }, + { + "pk4_entry_name": "Goods.dsc", + "payload_len": 18, + "payload_len_hex": "0x12", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Goods", + "visible_name": "Goods", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Grain.dsc", + "payload_len": 18, + "payload_len_hex": "0x12", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Grain", + "visible_name": "Grain", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4482Gravel.dsc", + "payload_len": 24, + "payload_len_hex": "0x18", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4482Gravel", + "visible_name": "Gravel", + "localized_string_id": 4482 + } + }, + { + "pk4_entry_name": "Iron.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Iron", + "visible_name": "Iron", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Livestock.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Livestock", + "visible_name": "Livestock", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Logs.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Logs", + "visible_name": "Logs", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Lumber.dsc", + "payload_len": 19, + "payload_len_hex": "0x13", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Lumber", + "visible_name": "Lumber", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Mail.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Mail", + "visible_name": "Mail", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Meat.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Meat", + "visible_name": "Meat", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Milk.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Milk", + "visible_name": "Milk", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4495Money.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4495Money", + "visible_name": "Money", + "localized_string_id": 4495 + } + }, + { + "pk4_entry_name": "~4497Newspaper.dsc", + "payload_len": 27, + "payload_len_hex": "0x1b", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4497Newspaper", + "visible_name": "Newspaper", + "localized_string_id": 4497 + } + }, + { + "pk4_entry_name": "Oil.dsc", + "payload_len": 16, + "payload_len_hex": "0x10", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Oil", + "visible_name": "Oil", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4507Paint.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4507Paint", + "visible_name": "Paint", + "localized_string_id": 4507 + } + }, + { + "pk4_entry_name": "Paper.dsc", + "payload_len": 18, + "payload_len_hex": "0x12", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Paper", + "visible_name": "Paper", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Passengers.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Passengers", + "visible_name": "Passengers", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4508Perfume.dsc", + "payload_len": 25, + "payload_len_hex": "0x19", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4508Perfume", + "visible_name": "Perfume", + "localized_string_id": 4508 + } + }, + { + "pk4_entry_name": "Plastic.dsc", + "payload_len": 20, + "payload_len_hex": "0x14", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Plastic", + "visible_name": "Plastic", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4509Potash.dsc", + "payload_len": 24, + "payload_len_hex": "0x18", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4509Potash", + "visible_name": "Potash", + "localized_string_id": 4509 + } + }, + { + "pk4_entry_name": "~4510Pottery.dsc", + "payload_len": 25, + "payload_len_hex": "0x19", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4510Pottery", + "visible_name": "Pottery", + "localized_string_id": 4510 + } + }, + { + "pk4_entry_name": "~4511Prisoners.dsc", + "payload_len": 27, + "payload_len_hex": "0x1b", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4511Prisoners", + "visible_name": "Prisoners", + "localized_string_id": 4511 + } + }, + { + "pk4_entry_name": "Produce.dsc", + "payload_len": 20, + "payload_len_hex": "0x14", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Produce", + "visible_name": "Produce", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Pulpwood.dsc", + "payload_len": 21, + "payload_len_hex": "0x15", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Pulpwood", + "visible_name": "Pulpwood", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Rice.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Rice", + "visible_name": "Rice", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Rubber.dsc", + "payload_len": 19, + "payload_len_hex": "0x13", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Rubber", + "visible_name": "Rubber", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4515Salt.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4515Salt", + "visible_name": "Salt", + "localized_string_id": 4515 + } + }, + { + "pk4_entry_name": "~4516Sand.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4516Sand", + "visible_name": "Sand", + "localized_string_id": 4516 + } + }, + { + "pk4_entry_name": "~4518Spaceships.dsc", + "payload_len": 28, + "payload_len_hex": "0x1c", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4518Spaceships", + "visible_name": "Spaceships", + "localized_string_id": 4518 + } + }, + { + "pk4_entry_name": "Steel.dsc", + "payload_len": 18, + "payload_len_hex": "0x12", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Steel", + "visible_name": "Steel", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Sugar.dsc", + "payload_len": 18, + "payload_len_hex": "0x12", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Sugar", + "visible_name": "Sugar", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4520Syrup.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4520Syrup", + "visible_name": "Syrup", + "localized_string_id": 4520 + } + }, + { + "pk4_entry_name": "~4523Tea.dsc", + "payload_len": 21, + "payload_len_hex": "0x15", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4523Tea", + "visible_name": "Tea", + "localized_string_id": 4523 + } + }, + { + "pk4_entry_name": "~4525Tin.dsc", + "payload_len": 21, + "payload_len_hex": "0x15", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4525Tin", + "visible_name": "Tin", + "localized_string_id": 4525 + } + }, + { + "pk4_entry_name": "Tires.dsc", + "payload_len": 18, + "payload_len_hex": "0x12", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Tires", + "visible_name": "Tires", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4529Tobacco.dsc", + "payload_len": 25, + "payload_len_hex": "0x19", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4529Tobacco", + "visible_name": "Tobacco", + "localized_string_id": 4529 + } + }, + { + "pk4_entry_name": "~4530Tools.dsc", + "payload_len": 23, + "payload_len_hex": "0x17", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4530Tools", + "visible_name": "Tools", + "localized_string_id": 4530 + } + }, + { + "pk4_entry_name": "Toys.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Toys", + "visible_name": "Toys", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Troops.dsc", + "payload_len": 19, + "payload_len_hex": "0x13", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Troops", + "visible_name": "Troops", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Uranium.dsc", + "payload_len": 20, + "payload_len_hex": "0x14", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Uranium", + "visible_name": "Uranium", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4538Valuables.dsc", + "payload_len": 27, + "payload_len_hex": "0x1b", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4538Valuables", + "visible_name": "Valuables", + "localized_string_id": 4538 + } + }, + { + "pk4_entry_name": "Waste.dsc", + "payload_len": 18, + "payload_len_hex": "0x12", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Waste", + "visible_name": "Waste", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "Weapons.dsc", + "payload_len": 20, + "payload_len_hex": "0x14", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Weapons", + "visible_name": "Weapons", + "localized_string_id": null + } + }, + { + "pk4_entry_name": "~4541Wine.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4541Wine", + "visible_name": "Wine", + "localized_string_id": 4541 + } + }, + { + "pk4_entry_name": "~4542Wire.dsc", + "payload_len": 22, + "payload_len_hex": "0x16", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "~4542Wire", + "visible_name": "Wire", + "localized_string_id": 4542 + } + }, + { + "pk4_entry_name": "Wool.dsc", + "payload_len": 17, + "payload_len_hex": "0x11", + "descriptor_kind": "cargoSkin", + "name": { + "raw_name": "Wool", + "visible_name": "Wool", + "localized_string_id": null + } + } + ] +} diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index c9ff173..7ccc472 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -17,13 +17,15 @@ use rrt_model::{ load_binary_summary, load_function_map, }; use rrt_runtime::{ - CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, OBSERVED_CAMPAIGN_SCENARIO_NAMES, + 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, 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, @@ -139,6 +141,16 @@ enum Command { RuntimeInspectPk4 { pk4_path: PathBuf, }, + RuntimeInspectCargoTypes { + cargo_types_dir: PathBuf, + }, + RuntimeInspectCargoSkins { + cargo_skin_pk4_path: PathBuf, + }, + RuntimeInspectCargoEconomySources { + cargo_types_dir: PathBuf, + cargo_skin_pk4_path: PathBuf, + }, RuntimeInspectWin { win_path: PathBuf, }, @@ -272,6 +284,25 @@ struct RuntimePk4InspectionOutput { inspection: Pk4InspectionReport, } +#[derive(Debug, Serialize)] +struct RuntimeCargoTypeInspectionOutput { + path: String, + inspection: CargoTypeInspectionReport, +} + +#[derive(Debug, Serialize)] +struct RuntimeCargoSkinInspectionOutput { + path: String, + inspection: CargoSkinInspectionReport, +} + +#[derive(Debug, Serialize)] +struct RuntimeCargoEconomyInspectionOutput { + cargo_types_dir: String, + cargo_skin_pk4_path: String, + inspection: CargoEconomySourceReport, +} + #[derive(Debug, Serialize)] struct RuntimeWinInspectionOutput { path: String, @@ -815,6 +846,20 @@ fn real_main() -> Result<(), Box> { Command::RuntimeInspectPk4 { pk4_path } => { run_runtime_inspect_pk4(&pk4_path)?; } + Command::RuntimeInspectCargoTypes { cargo_types_dir } => { + run_runtime_inspect_cargo_types(&cargo_types_dir)?; + } + Command::RuntimeInspectCargoSkins { + cargo_skin_pk4_path, + } => { + run_runtime_inspect_cargo_skins(&cargo_skin_pk4_path)?; + } + Command::RuntimeInspectCargoEconomySources { + cargo_types_dir, + cargo_skin_pk4_path, + } => { + run_runtime_inspect_cargo_economy_sources(&cargo_types_dir, &cargo_skin_pk4_path)?; + } Command::RuntimeInspectWin { win_path } => { run_runtime_inspect_win(&win_path)?; } @@ -993,6 +1038,28 @@ fn parse_command() -> Result> { pk4_path: PathBuf::from(path), }) } + [command, subcommand, path] + if command == "runtime" && subcommand == "inspect-cargo-types" => + { + Ok(Command::RuntimeInspectCargoTypes { + cargo_types_dir: PathBuf::from(path), + }) + } + [command, subcommand, path] + if command == "runtime" && subcommand == "inspect-cargo-skins" => + { + Ok(Command::RuntimeInspectCargoSkins { + cargo_skin_pk4_path: PathBuf::from(path), + }) + } + [command, subcommand, cargo_types_dir, cargo_skin_pk4_path] + if command == "runtime" && subcommand == "inspect-cargo-economy-sources" => + { + Ok(Command::RuntimeInspectCargoEconomySources { + 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), @@ -1128,7 +1195,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-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-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(), ), } @@ -1490,6 +1557,41 @@ fn run_runtime_inspect_pk4(pk4_path: &Path) -> Result<(), Box Result<(), Box> { + let report = RuntimeCargoTypeInspectionOutput { + path: cargo_types_dir.display().to_string(), + inspection: inspect_cargo_types_dir(cargo_types_dir)?, + }; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} + +fn run_runtime_inspect_cargo_skins( + cargo_skin_pk4_path: &Path, +) -> Result<(), Box> { + let report = RuntimeCargoSkinInspectionOutput { + path: cargo_skin_pk4_path.display().to_string(), + inspection: inspect_cargo_skin_pk4(cargo_skin_pk4_path)?, + }; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} + +fn run_runtime_inspect_cargo_economy_sources( + cargo_types_dir: &Path, + cargo_skin_pk4_path: &Path, +) -> Result<(), Box> { + let report = RuntimeCargoEconomyInspectionOutput { + cargo_types_dir: cargo_types_dir.display().to_string(), + cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(), + inspection: inspect_cargo_economy_sources(cargo_types_dir, cargo_skin_pk4_path)?, + }; + 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(), diff --git a/crates/rrt-model/src/lib.rs b/crates/rrt-model/src/lib.rs index d3fd4db..2641585 100644 --- a/crates/rrt-model/src/lib.rs +++ b/crates/rrt-model/src/lib.rs @@ -33,6 +33,7 @@ pub const REQUIRED_EXPORTS: &[&str] = &[ "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", + "artifacts/exports/rt3-1.06/economy-cargo-sources.json", ]; pub const REQUIRED_ATLAS_HEADINGS: &[&str] = &[ diff --git a/crates/rrt-runtime/src/economy.rs b/crates/rrt-runtime/src/economy.rs new file mode 100644 index 0000000..c30d8a2 --- /dev/null +++ b/crates/rrt-runtime/src/economy.rs @@ -0,0 +1,417 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::pk4::inspect_pk4_bytes; + +pub const NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT: usize = 71; +pub const NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT: usize = 50; +pub const CARGO_TYPE_MAGIC: u32 = 0x0000_03ea; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoNameToken { + pub raw_name: String, + pub visible_name: String, + #[serde(default)] + pub localized_string_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoTypeEntry { + pub file_name: String, + pub file_size: usize, + pub file_size_hex: String, + pub header_magic: u32, + pub header_magic_hex: String, + pub name: CargoNameToken, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoTypeInspectionReport { + pub directory_path: String, + pub entry_count: usize, + pub unique_visible_name_count: usize, + pub notes: Vec, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoSkinDescriptorEntry { + pub pk4_entry_name: String, + pub payload_len: usize, + pub payload_len_hex: String, + pub descriptor_kind: String, + pub name: CargoNameToken, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoSkinInspectionReport { + pub pk4_path: String, + pub entry_count: usize, + pub unique_visible_name_count: usize, + pub notes: Vec, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CargoEconomySourceReport { + pub cargo_types_dir: String, + pub cargo_skin_pk4_path: String, + pub named_cargo_price_row_count: usize, + pub named_cargo_production_row_count: usize, + pub cargo_type_count: usize, + pub cargo_skin_count: usize, + pub shared_visible_name_count: usize, + pub visible_name_union_count: usize, + pub cargo_type_only_visible_names: Vec, + pub cargo_skin_only_visible_names: Vec, + pub notes: Vec, + pub cargo_type_entries: Vec, + pub cargo_skin_entries: Vec, +} + +pub fn inspect_cargo_types_dir( + path: &Path, +) -> Result> { + let mut entries = Vec::new(); + for entry in fs::read_dir(path)? { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + let file_name = entry.file_name(); + let file_name = file_name.to_string_lossy().into_owned(); + if Path::new(&file_name) + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| extension.eq_ignore_ascii_case("cty")) + != Some(true) + { + continue; + } + let bytes = fs::read(entry.path())?; + entries.push(parse_cargo_type_entry(&file_name, &bytes)?); + } + entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name)); + + let mut notes = Vec::new(); + notes.push( + "CargoTypes entries carry a 0x03ea header and an inline NUL-terminated cargo token string." + .to_string(), + ); + notes.push( + "A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix." + .to_string(), + ); + + let unique_visible_name_count = entries + .iter() + .map(|entry| entry.name.visible_name.as_str()) + .collect::>() + .len(); + + Ok(CargoTypeInspectionReport { + directory_path: path.display().to_string(), + entry_count: entries.len(), + unique_visible_name_count, + notes, + entries, + }) +} + +pub fn inspect_cargo_skin_pk4( + path: &Path, +) -> Result> { + let bytes = fs::read(path)?; + let inspection = inspect_pk4_bytes(&bytes)?; + let mut entries = Vec::new(); + for entry in &inspection.entries { + if entry.extension.as_deref() != Some("dsc") { + continue; + } + let payload = bytes + .get(entry.payload_absolute_offset..entry.payload_end_offset) + .ok_or_else(|| format!("pk4 payload range out of bounds for {}", entry.name))?; + let parsed = parse_cargo_skin_descriptor_entry(&entry.name, payload)?; + entries.push(parsed); + } + entries.sort_by(|left, right| left.name.visible_name.cmp(&right.name.visible_name)); + + let mut notes = Vec::new(); + notes.push( + "cargoSkin descriptors are parsed from .dsc payloads whose first non-empty line names the descriptor kind and whose second non-empty line carries the cargo token." + .to_string(), + ); + notes.push( + "A leading `~####` token is preserved as raw_name and normalized into visible_name by stripping the localized string id prefix." + .to_string(), + ); + + let unique_visible_name_count = entries + .iter() + .map(|entry| entry.name.visible_name.as_str()) + .collect::>() + .len(); + + Ok(CargoSkinInspectionReport { + pk4_path: path.display().to_string(), + entry_count: entries.len(), + unique_visible_name_count, + notes, + entries, + }) +} + +pub fn inspect_cargo_economy_sources( + cargo_types_dir: &Path, + cargo_skin_pk4_path: &Path, +) -> Result> { + let cargo_types = inspect_cargo_types_dir(cargo_types_dir)?; + let cargo_skins = inspect_cargo_skin_pk4(cargo_skin_pk4_path)?; + Ok(build_cargo_economy_source_report(cargo_types, cargo_skins)) +} + +fn build_cargo_economy_source_report( + cargo_types: CargoTypeInspectionReport, + cargo_skins: CargoSkinInspectionReport, +) -> CargoEconomySourceReport { + let cargo_type_visible_names = cargo_types + .entries + .iter() + .map(|entry| entry.name.visible_name.clone()) + .collect::>(); + let cargo_skin_visible_names = cargo_skins + .entries + .iter() + .map(|entry| entry.name.visible_name.clone()) + .collect::>(); + + let shared_visible_name_count = cargo_type_visible_names + .intersection(&cargo_skin_visible_names) + .count(); + let visible_name_union_count = cargo_type_visible_names + .union(&cargo_skin_visible_names) + .count(); + let cargo_type_only_visible_names = cargo_type_visible_names + .difference(&cargo_skin_visible_names) + .cloned() + .collect::>(); + let cargo_skin_only_visible_names = cargo_skin_visible_names + .difference(&cargo_type_visible_names) + .cloned() + .collect::>(); + + let mut notes = Vec::new(); + notes.push(format!( + "Named cargo-price descriptors 106..176 span {} rows, while named cargo-production descriptors 180..229 span {} rows.", + NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT + )); + notes.push(format!( + "The inspected CargoTypes corpus exposes {} visible names, the inspected cargoSkin corpus exposes {} visible names, and their union exposes {} visible names.", + cargo_types.unique_visible_name_count, cargo_skins.unique_visible_name_count, visible_name_union_count + )); + if visible_name_union_count != NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT { + notes.push( + "That visible-name union still does not match the 71-row named cargo-price strip, so this offline source reconstruction is groundwork rather than a complete price-selector binding." + .to_string(), + ); + } else { + notes.push( + "The visible-name union matches the 71-row named cargo-price strip; a later pass can decide whether ordering evidence is now strong enough for descriptor bindings." + .to_string(), + ); + } + if cargo_types.unique_visible_name_count == NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT { + notes.push( + "The CargoTypes corpus still matches the 50-row named cargo-production strip cardinality that grounds the current production bindings." + .to_string(), + ); + } + + CargoEconomySourceReport { + cargo_types_dir: cargo_types.directory_path, + cargo_skin_pk4_path: cargo_skins.pk4_path, + named_cargo_price_row_count: NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, + named_cargo_production_row_count: NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT, + cargo_type_count: cargo_types.entry_count, + cargo_skin_count: cargo_skins.entry_count, + shared_visible_name_count, + visible_name_union_count, + cargo_type_only_visible_names, + cargo_skin_only_visible_names, + notes, + cargo_type_entries: cargo_types.entries, + cargo_skin_entries: cargo_skins.entries, + } +} + +fn parse_cargo_type_entry( + file_name: &str, + bytes: &[u8], +) -> Result> { + if bytes.len() < 5 { + return Err(format!("cargo type entry {file_name} is too short").into()); + } + let header_magic = u32::from_le_bytes(bytes[0..4].try_into().expect("length checked")); + let raw_name = parse_nul_terminated_utf8(bytes, 4) + .ok_or_else(|| format!("cargo type entry {file_name} is missing a NUL-terminated name"))?; + + Ok(CargoTypeEntry { + file_name: file_name.to_string(), + file_size: bytes.len(), + file_size_hex: format!("0x{:x}", bytes.len()), + header_magic, + header_magic_hex: format!("0x{header_magic:08x}"), + name: parse_cargo_name_token(&raw_name), + }) +} + +fn parse_cargo_skin_descriptor_entry( + entry_name: &str, + bytes: &[u8], +) -> Result> { + let text = std::str::from_utf8(bytes)?; + let mut lines = text.lines().map(str::trim).filter(|line| !line.is_empty()); + let descriptor_kind = lines + .next() + .ok_or_else(|| format!("cargo skin descriptor {entry_name} is missing the kind line"))?; + let raw_name = lines + .next() + .ok_or_else(|| format!("cargo skin descriptor {entry_name} is missing the name line"))?; + + Ok(CargoSkinDescriptorEntry { + pk4_entry_name: entry_name.to_string(), + payload_len: bytes.len(), + payload_len_hex: format!("0x{:x}", bytes.len()), + descriptor_kind: descriptor_kind.to_string(), + name: parse_cargo_name_token(raw_name), + }) +} + +fn parse_nul_terminated_utf8(bytes: &[u8], offset: usize) -> Option { + let tail = bytes.get(offset..)?; + let end = tail.iter().position(|byte| *byte == 0)?; + String::from_utf8(tail[..end].to_vec()).ok() +} + +fn parse_cargo_name_token(raw_name: &str) -> CargoNameToken { + let mut visible_name = raw_name.to_string(); + let mut localized_string_id = None; + if let Some(rest) = raw_name.strip_prefix('~') { + let digits = rest + .chars() + .take_while(|character| character.is_ascii_digit()) + .collect::(); + if !digits.is_empty() { + localized_string_id = digits.parse::().ok(); + visible_name = rest[digits.len()..].to_string(); + } + } + + CargoNameToken { + raw_name: raw_name.to_string(), + visible_name, + localized_string_id, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_plain_cargo_type_entry() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&CARGO_TYPE_MAGIC.to_le_bytes()); + bytes.extend_from_slice(b"Alcohol\0"); + let entry = parse_cargo_type_entry("Alcohol.cty", &bytes).expect("entry should parse"); + assert_eq!(entry.header_magic, CARGO_TYPE_MAGIC); + assert_eq!(entry.name.raw_name, "Alcohol"); + assert_eq!(entry.name.visible_name, "Alcohol"); + assert_eq!(entry.name.localized_string_id, None); + } + + #[test] + fn parses_localized_cargo_type_entry() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&CARGO_TYPE_MAGIC.to_le_bytes()); + bytes.extend_from_slice(b"~4465Ceramics\0"); + let entry = + parse_cargo_type_entry("~4465Ceramics.cty", &bytes).expect("entry should parse"); + assert_eq!(entry.name.raw_name, "~4465Ceramics"); + assert_eq!(entry.name.visible_name, "Ceramics"); + assert_eq!(entry.name.localized_string_id, Some(4465)); + } + + #[test] + fn parses_cargo_skin_descriptor_entry() { + let entry = parse_cargo_skin_descriptor_entry("Alcohol.dsc", b"cargoSkin\r\nAlcohol\r\n") + .expect("descriptor should parse"); + assert_eq!(entry.descriptor_kind, "cargoSkin"); + assert_eq!(entry.name.visible_name, "Alcohol"); + } + + #[test] + fn builds_cargo_source_report_union_counts() { + let cargo_types = CargoTypeInspectionReport { + directory_path: "CargoTypes".to_string(), + entry_count: 2, + unique_visible_name_count: 2, + notes: Vec::new(), + entries: vec![ + CargoTypeEntry { + file_name: "Alcohol.cty".to_string(), + file_size: 16, + file_size_hex: "0x10".to_string(), + header_magic: CARGO_TYPE_MAGIC, + header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), + name: parse_cargo_name_token("Alcohol"), + }, + CargoTypeEntry { + file_name: "Coal.cty".to_string(), + file_size: 16, + file_size_hex: "0x10".to_string(), + header_magic: CARGO_TYPE_MAGIC, + header_magic_hex: format!("0x{CARGO_TYPE_MAGIC:08x}"), + name: parse_cargo_name_token("Coal"), + }, + ], + }; + let cargo_skins = CargoSkinInspectionReport { + pk4_path: "Cargo106.PK4".to_string(), + entry_count: 2, + unique_visible_name_count: 2, + notes: Vec::new(), + entries: vec![ + CargoSkinDescriptorEntry { + pk4_entry_name: "Alcohol.dsc".to_string(), + payload_len: 20, + payload_len_hex: "0x14".to_string(), + descriptor_kind: "cargoSkin".to_string(), + name: parse_cargo_name_token("Alcohol"), + }, + CargoSkinDescriptorEntry { + pk4_entry_name: "Beer.dsc".to_string(), + payload_len: 16, + payload_len_hex: "0x10".to_string(), + descriptor_kind: "cargoSkin".to_string(), + name: parse_cargo_name_token("Beer"), + }, + ], + }; + + let report = build_cargo_economy_source_report(cargo_types, cargo_skins); + assert_eq!(report.shared_visible_name_count, 1); + assert_eq!(report.visible_name_union_count, 3); + assert_eq!( + report.cargo_type_only_visible_names, + vec!["Coal".to_string()] + ); + assert_eq!( + report.cargo_skin_only_visible_names, + vec!["Beer".to_string()] + ); + } +} diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 38bdca5..6ce0ccb 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -1,5 +1,6 @@ pub mod calendar; pub mod campaign_exe; +pub mod economy; pub mod import; pub mod persistence; pub mod pk4; @@ -14,6 +15,12 @@ pub use campaign_exe::{ CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, CampaignPageBand, CampaignScenarioEntry, OBSERVED_CAMPAIGN_SCENARIO_NAMES, inspect_campaign_exe_bytes, inspect_campaign_exe_file, }; +pub use economy::{ + CARGO_TYPE_MAGIC, CargoEconomySourceReport, CargoNameToken, CargoSkinDescriptorEntry, + CargoSkinInspectionReport, CargoTypeEntry, CargoTypeInspectionReport, + NAMED_CARGO_PRICE_DESCRIPTOR_ROW_COUNT, NAMED_CARGO_PRODUCTION_DESCRIPTOR_ROW_COUNT, + inspect_cargo_economy_sources, inspect_cargo_skin_pk4, inspect_cargo_types_dir, +}; pub use import::{ OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument, RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource, diff --git a/docs/README.md b/docs/README.md index 837abe9..7b4ad78 100644 --- a/docs/README.md +++ b/docs/README.md @@ -130,7 +130,12 @@ The highest-value next passes are now: but the checked-in semantic catalog now gives that band stable `Named Cargo Price Slot N` labels instead of anonymous `Unknown Cargo Price` residue; the checked-in static CargoTypes corpora also make the current limit explicit because the broader 1.06 corpus has `51` names and - the 1.05 corpus has `41`, while the named price strip still spans `71` descriptors + the 1.05 corpus has `41`, while the named price strip still spans `71` descriptors; a new + checked-in offline cargo-source report at + `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, and 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 49c848b..6e9fd70 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -91,7 +91,12 @@ Implemented today: the checked-in semantic catalog now at least gives that band stable `Named Cargo Price Slot N` labels instead of anonymous `Unknown Cargo Price` residue, and the checked-in static CargoTypes corpora now make the evidence gap explicit because they cover `51` names in the broader 1.06 - install and `41` in the 1.05 install while the named price strip still spans `71` descriptors + install and `41` in the 1.05 install while the named price strip still spans `71` descriptors; + a new checked-in offline cargo-source report at + `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, and 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