From 764b61cc7806043a4fe57b9137a2788a4d5cb789 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:54:05 +0100 Subject: [PATCH 1/3] Bump actions/checkout from 4 to 5 (#22) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check.yml | 2 +- .github/workflows/clippy.yml | 2 +- .github/workflows/rustfmt.yml | 2 +- .github/workflows/tests.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 78bc369..97a30c1 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -10,7 +10,7 @@ jobs: name: Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions-rs/toolchain@v1 with: profile: minimal diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 4349927..4b783de 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -11,7 +11,7 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions-rs/toolchain@v1 with: profile: minimal diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml index cae854c..eb3345a 100644 --- a/.github/workflows/rustfmt.yml +++ b/.github/workflows/rustfmt.yml @@ -10,7 +10,7 @@ jobs: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions-rs/toolchain@v1 with: profile: minimal diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 34a80e5..614a6f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: name: Test Suite runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions-rs/toolchain@v1 with: profile: minimal From 878496c3b5f7829a79f5e618e03fe278a0a17a32 Mon Sep 17 00:00:00 2001 From: BloodStainedCrow Date: Sat, 8 Nov 2025 11:03:49 +0100 Subject: [PATCH 2/3] Release v0.2.1 (#26) * Change beacons to scale linearly every 60 ticks (instead of a binary on/off) * add default hotbar assignment * Add Accumulator sprite --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 50 ++----- src/app_state.rs | 66 ++++----- src/assembler/bucketed.rs | 4 + src/assembler/mod.rs | 54 ++++++++ src/assembler/simd.rs | 61 ++------- src/belt/mod.rs | 30 +++-- src/belt/smart.rs | 29 +++- src/belt/sushi.rs | 66 ++++++--- src/data/mod.rs | 35 ++--- src/frontend/action/action_state_machine.rs | 73 +++++++++- src/frontend/world/tile.rs | 35 ++--- src/inserter/belt_storage_inserter.rs | 6 +- .../belt_storage_inserter_non_const_gen.rs | 7 +- src/inserter/belt_storage_pure_buckets.rs | 17 ++- src/inserter/mod.rs | 127 +++++++----------- src/inserter/storage_storage_inserter.rs | 7 +- src/inserter/storage_storage_with_buckets.rs | 6 +- .../storage_storage_with_buckets_indirect.rs | 8 +- src/lab.rs | 2 +- src/liquid/mod.rs | 11 +- src/par_generation.rs | 3 +- src/power/mod.rs | 64 +++++++-- src/power/power_grid.rs | 89 +++++++++--- src/rendering/eframe_app.rs | 2 +- src/rendering/mod.rs | 5 + src/rendering/render_world.rs | 20 +-- .../temp_assets/krastorio/energy-storage.png | Bin 0 -> 71532 bytes src/storage_list.rs | 92 ++++++++++++- test_blueprints/solar_tile.bp | 4 +- 31 files changed, 643 insertions(+), 334 deletions(-) create mode 100644 src/rendering/temp_assets/krastorio/energy-storage.png diff --git a/Cargo.lock b/Cargo.lock index cbbc526..e2e6601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1692,7 +1692,7 @@ dependencies = [ [[package]] name = "factory" -version = "0.2.0" +version = "0.2.1" dependencies = [ "base64 0.22.1", "bimap", diff --git a/Cargo.toml b/Cargo.toml index 9b0d06b..37f5add 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # cargo-features = ["codegen-backend"] [package] name = "factory" -version = "0.2.0" +version = "0.2.1" edition = "2024" rust-version = "1.85" build = "build.rs" diff --git a/README.md b/README.md index c19a723..b70f16a 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,18 @@ # What is this? -This project is an academic recreation of the factory game [Factorio](https://www.factorio.com/) taking additional ideas from [Dyson Sphere Program](https://store.steampowered.com/app/1366540/Dyson_Sphere_Program/). +This project is an academic recreation of the factory game [Factorio](https://www.factorio.com/). -I created it as an exercise to see how far I could optimize the basic concepts and algorithms of the genre in terms of performance, while allowing myself minor changes to the games' rules. +I created it as an exercise to see how far I could optimize the basic mechanics and algorithms of the genre in terms of performance. Another goal that emerged along the way, was learning about the way modern CPUs actually work. -# Roadmap -Currently I adding beacons and thinking about how to efficiently add logistics bots. Then I want to build a comprehensive suit of benchmark test to show if/by how much I was able to improve performance. - # Why did you start? -I was playing the above games and started being unable to expand due to performance issues. So in my hubris I declared: "How hard can it be?". +I was playing Factorio and started being unable to expand due to performance issues. So in my hubris I declared: "How hard can it be?". + +# Current State +Most logic for power grids, belts, splitters, assemblers, labs, inserters, mining drills, solar panels and accumulators is working. This allowed me to recreate a Factorio base, giving me a point for performance comparison. +I was able to run a base comprised of 40 copies of [this](https://factoriobox.1au.us/map/view/2824bc1566bd95b5825baf3bd2eb8fa32de8397526464f5a0327bcb82d64ebf8/#1/nauvis/15/2942/1158/0/447) Factorio Megabase by Smurphy (which Factorio runs at ~40 UPS) at 60 UPS on my machine. + +# Running it +It should run on Linux, Windows and MacOS. Assuming you have [rust and cargo](https://rust-lang.org), just `cargo run --release`. On NixOS the included `shell.nix` contains all you need. -## TODOS -- ~~Place Power Production~~ -- ~~Blueprints so I can actually do perf tests~~ -- ~~Permanently running replay system, so I can easily recreate crashes~~ -- ~~Test harness for replays, to ensure they do not crash~~ -- ~~Automatic insertion limit~~ -- ~~Assembler Module Support~~ -- ~~World listener support (i.e. update whenever something changes in the world, for power, beacons and inserters)~~ -- Lazy Terrain Generation -- ~~Assembler Module Frontend~~ -- ~~Assembler Power Consumption Modifier Support~~ -- ~~Beacons~~ -- ~~FIX Beacon Flicker due to lowering power consumption when beacons are unpowered~~ -- ~~Storage Storage Inserters~~ -- ~~Science Consumption in Labs~~ -- ~~Inserter connections to labs~~ -- ~~Debug inserters~~ -- ~~Production Graphs~~ -- ~~Liquids~~ -- ~~Map View~~ -- ~~Technology~~ -- Mining Drills -- ~~Underground belts~~ -- Fix Underground Pipe connection breaking/overlap -- Place Steam Turbines -- ~~Splitters~~ -- Allow Belts of different types to connect to one another -- Decide if I want beacons to match factorio behaviour or keep the hard switch on/off -- ~~Ore Generation~~ -- Add tile requirements for buildings/recipes (for offshore pump) -- Bots -- MAYBE: A canonical version of the simulation that can be used for diff testing (and as some weird documentation of the mechanics I suppose) \ No newline at end of file +# Attributions +All graphics used with the `graphics` feature are from the Factorio Mod [Krastorio 2 Assets](https://codeberg.org/raiguard/Krastorio2Assets). \ No newline at end of file diff --git a/src/app_state.rs b/src/app_state.rs index c819a19..2ac4173 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -746,8 +746,10 @@ impl StorageStorageInserterStore { hand_size: ITEMCOUNTTYPE, data_store: &DataStore, ) -> InserterIdentifier { - let source = FakeUnionStorage::from_storage_with_statics_at_zero(item, start, data_store); - let dest = FakeUnionStorage::from_storage_with_statics_at_zero(item, dest, data_store); + let source = + FakeUnionStorage::from_storage_with_statics_at_zero(item, start, data_store).unwrap(); + let dest = + FakeUnionStorage::from_storage_with_statics_at_zero(item, dest, data_store).unwrap(); let id: InserterIdentifier = self.inserters[item.into_usize()] .entry(movetime) @@ -829,7 +831,8 @@ impl StorageStorageInserterStore { .0 .update_inserter_src( id, - FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store), + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store) + .unwrap(), ) } @@ -848,7 +851,8 @@ impl StorageStorageInserterStore { .0 .update_inserter_dest( id, - FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store), + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store) + .unwrap(), ) } @@ -869,7 +873,8 @@ impl StorageStorageInserterStore { .update_inserter_src_if_equal( id, old_src, - FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store), + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store) + .unwrap(), ) } @@ -890,7 +895,8 @@ impl StorageStorageInserterStore { .update_inserter_dest_if_equal( id, old_dest, - FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store), + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store) + .unwrap(), ) } } @@ -1137,8 +1143,7 @@ impl Factory GameState { // Handle storage updates for storage_update in storage_updates { - let mut entity_size = None; - game_state.world.get_entity_at_mut(storage_update.position, data_store).map(|e| { - match (e, storage_update.new_pg_entity.clone()) { - (Entity::Assembler { ty, pos: _, info: AssemblerInfo::Powered { id, pole_position: _, weak_index: _ }, modules: _, rotation }, crate::power::power_grid::PowerGridEntity::Assembler { ty: _, recipe, index }) => { - entity_size = Some(data_store.assembler_info[usize::from(*ty)].size(*rotation)); + let entity = game_state.world.get_entity_at_mut(storage_update.position, data_store).expect(&format!("Did not find entity at {:?} for storage update", storage_update.position)); + + let entity_size = match (entity, storage_update.new_pg_entity.clone()) { + (Entity::Assembler { + ty, + pos: _, + info: AssemblerInfo::Powered { id, pole_position: _, weak_index: _ }, modules: _, rotation }, + crate::power::power_grid::PowerGridEntity::Assembler { ty: _, recipe, index }) => { assert_eq!(id.recipe, recipe); id.grid = storage_update.new_grid; id.assembler_index = index; // FIXME: Store and update the weak_index + + data_store.assembler_info[usize::from(*ty)].size(*rotation) }, (Entity::Lab { pos: _, ty, modules: _, pole_position: Some((_pole_pos, _weak_idx, lab_store_index)) }, crate::power::power_grid::PowerGridEntity::Lab { ty: _, index: new_idx }) => { - entity_size = Some(data_store.lab_info[usize::from(*ty)].size); *lab_store_index = new_idx; // The weak index stays the same since it it still connected to the same power pole + + data_store.lab_info[usize::from(*ty)].size } (_, _) => todo!("Handler storage_update {storage_update:?}") - } - }); + }; // FIXME: Rotation - let e_size = entity_size.unwrap(); + let e_size = entity_size; let inserter_range = data_store.max_inserter_search_range; @@ -1702,7 +1712,7 @@ impl GameState unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), data_store).unwrap(); + }; game_state.simulation_state.factory.belts.update_belt_storage_inserter_src(*id, *belt_pos, game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), new_storage, data_store); }, AttachedInserter::BeltBelt { .. } => { @@ -1716,7 +1726,7 @@ impl GameState unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(*item, data_store).unwrap(); + }; let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks); @@ -1740,7 +1750,7 @@ impl GameState unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), data_store).unwrap(); + }; game_state.simulation_state.factory.belts.update_belt_storage_inserter_dest(*id, *belt_pos, game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), new_storage, data_store); }, AttachedInserter::BeltBelt { .. } => { @@ -1754,7 +1764,7 @@ impl GameState unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(*item, data_store).unwrap(); + }; let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks); let new_id = game_state.simulation_state.factory.storage_storage_inserters.update_inserter_dest(*item, movetime.into(), *inserter, new_storage, data_store); @@ -1768,7 +1778,7 @@ impl GameState { let id: FluidSystemId<_> = game_state.simulation_state.factory.fluid_store.fluid_box_pos_to_network_id[pos]; if let Some(fluid) = id.fluid { - let storage = match storage_update.new_pg_entity { + let storage = match storage_update.old_pg_entity { crate::power::power_grid::PowerGridEntity::Assembler { ty, recipe, index } => Storage::Assembler { grid: storage_update.old_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { ty, index } => Storage::Lab { grid: storage_update.old_grid, index }, crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), @@ -1776,13 +1786,9 @@ impl GameState unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), }; - dbg!(storage); - let Some(translated_storage) = storage.translate(fluid, data_store) else { + let Ok(old_storage) = FakeUnionStorage::from_storage_with_statics_at_zero(fluid, storage, data_store) else { return ControlFlow::Continue(()); }; - dbg!(translated_storage); - let old_storage = FakeUnionStorage::from_storage_with_statics_at_zero(fluid, translated_storage, data_store); - dbg!(old_storage); let new_storage = match storage_update.new_pg_entity { crate::power::power_grid::PowerGridEntity::Assembler { ty, recipe, index } => Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, @@ -1791,8 +1797,8 @@ impl GameState unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(fluid, data_store).unwrap(); - game_state.simulation_state.factory.fluid_store.update_fluid_conn_if_needed(*pos, old_storage, FakeUnionStorage::from_storage_with_statics_at_zero(fluid, new_storage, data_store)); + }; + game_state.simulation_state.factory.fluid_store.update_fluid_conn_if_needed(*pos, old_storage, FakeUnionStorage::from_storage_with_statics_at_zero(fluid, new_storage, data_store).unwrap()); } }, @@ -2333,7 +2339,7 @@ impl GameState ()>, diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index a92f38b..a36c9ab 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -877,4 +877,8 @@ impl usize { + self.hot_data.len() - self.holes.len() + } } diff --git a/src/assembler/mod.rs b/src/assembler/mod.rs index 6c35fa2..8640c61 100644 --- a/src/assembler/mod.rs +++ b/src/assembler/mod.rs @@ -505,6 +505,58 @@ impl< _ => unreachable!(), } } + + pub fn num_assemblers(&self) -> usize { + let Self { + assemblers_0_1, + assemblers_1_1, + assemblers_2_1, + assemblers_2_2, + assemblers_2_3, + assemblers_3_1, + assemblers_4_1, + assemblers_5_1, + assemblers_6_1, + recipe: _, + } = self; + + assemblers_0_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_1_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_2_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_2_2 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_2_3 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_3_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_4_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_5_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_6_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + } } // FIXME: @@ -799,6 +851,8 @@ pub trait MultiAssemblerStore< ret } + + fn num_assemblers(&self) -> usize; } pub mod arrays { diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index f3066a3..e108e61 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -665,6 +665,7 @@ impl; NUM_INGS] = self @@ -673,13 +674,7 @@ impl usize { + self.len - self.holes.len() + } } #[cfg(test)] diff --git a/src/belt/mod.rs b/src/belt/mod.rs index c9f81fa..fcfe7de 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -1539,7 +1539,7 @@ impl BeltStore { .inserters .inserters .iter() - .filter_map(|(ins, item)| { + .filter_map(|(ins, item, _movetime, _hand_size)| { let (dir, _state) = ins.state.into(); (dir == Dir::StorageToBelt).then_some(*item) }) @@ -2100,7 +2100,7 @@ impl BeltStore { belt.inserters .inserters .iter() - .filter_map(|(ins, item)| { + .filter_map(|(ins, item, _movetime, _hand_size)| { let (dir, _state) = ins.state.into(); (dir == Dir::StorageToBelt).then_some(*item) }) @@ -2598,8 +2598,9 @@ impl BeltStore { movetime: u16, hand_size: ITEMCOUNTTYPE, ) -> Result<(), SpaceOccupiedError> { - let handle_sushi_belt = - |belt: &mut SushiBelt| belt.add_in_inserter(filter, pos, storage_id); + let handle_sushi_belt = |belt: &mut SushiBelt| { + belt.add_in_inserter(filter, pos, storage_id, movetime, hand_size) + }; match id { BeltTileId::AnyBelt(index, _) => { @@ -2626,7 +2627,7 @@ impl BeltStore { let now_sushi_belt = self.inner.get_sushi_mut(new_index); now_sushi_belt - .add_in_inserter(filter, pos, storage_id) + .add_in_inserter(filter, pos, storage_id, movetime, hand_size) .expect("We already became sushi, it should now work!"); }, } @@ -2667,8 +2668,9 @@ impl BeltStore { movetime: u16, hand_size: ITEMCOUNTTYPE, ) -> Result<(), SpaceOccupiedError> { - let handle_sushi_belt = - |belt: &mut SushiBelt| belt.add_out_inserter(filter, pos, storage_id); + let handle_sushi_belt = |belt: &mut SushiBelt| { + belt.add_out_inserter(filter, pos, storage_id, movetime, hand_size) + }; match id { BeltTileId::AnyBelt(index, _) => { @@ -2695,7 +2697,7 @@ impl BeltStore { let now_sushi_belt = self.inner.get_sushi_mut(new_index); now_sushi_belt - .add_out_inserter(filter, pos, storage_id) + .add_out_inserter(filter, pos, storage_id, movetime, hand_size) .expect("We already became sushi, it should now work!"); }, } @@ -3010,7 +3012,8 @@ impl BeltStore { belt_id.item, new_src, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Sushi(index) => { @@ -3018,7 +3021,8 @@ impl BeltStore { belt_pos, FakeUnionStorage::from_storage_with_statics_at_zero( src_item, new_src, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Empty(_) => unimplemented!("Empty belt cannot have inserters"), @@ -3045,7 +3049,8 @@ impl BeltStore { belt_id.item, new_dest, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Sushi(index) => { @@ -3053,7 +3058,8 @@ impl BeltStore { belt_pos, FakeUnionStorage::from_storage_with_statics_at_zero( dest_item, new_dest, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Empty(_) => unimplemented!("Empty belt cannot have inserters"), diff --git a/src/belt/smart.rs b/src/belt/smart.rs index 0e93852..850106c 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -5,6 +5,7 @@ use std::{ u8, }; +use crate::item::Indexable; use crate::{ inserter::{ InserterState, belt_storage_inserter::Dir, @@ -46,10 +47,6 @@ pub static NUM_BELT_FREE_CACHE_HITS: AtomicUsize = AtomicUsize::new(0); // #[cfg(debug_assertions)] pub static NUM_BELT_LOCS_SEARCHED: AtomicUsize = AtomicUsize::new(0); -// HUGE FIXME: -pub const MOVETIME: u8 = 12; -pub const HAND_SIZE: u8 = 12; - #[allow(clippy::module_name_repetitions)] #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] @@ -98,6 +95,8 @@ pub struct BeltInserterInfo { pub outgoing: bool, pub state: InserterState, pub connection: FakeUnionStorage, + pub hand_size: ITEMCOUNTTYPE, + pub movetime: u8, } #[derive(Debug)] @@ -180,7 +179,7 @@ impl SmartBelt { inserters: SushiInserterStoreDyn { inserters: inserters .into_iter() - .map(|(inserter, _movetime, _hand_size)| (inserter, item)) + .map(|(inserter, movetime, hand_size)| (inserter, item, movetime, hand_size)) .collect(), }, @@ -347,6 +346,8 @@ impl SmartBelt { outgoing: dir == Dir::BeltToStorage, state, connection: inserter.0.storage_id, + movetime: inserter.1, + hand_size: inserter.2, }); } else if pos > belt_pos { return None; @@ -639,7 +640,14 @@ impl SmartBelt { // We KNOW this position is filled debug_assert!(self.locs[loc_idx]); let mut loc = true; - let _changed = ins.update(&mut loc, storages, *movetime, *hand_size, grid_size); + let _changed = ins.update( + self.item.into_usize(), + &mut loc, + storages, + *movetime, + *hand_size, + grid_size, + ); if !loc { self.locs.set(loc_idx, false); @@ -651,7 +659,14 @@ impl SmartBelt { } else { let mut loc = self.locs.get_mut(loc_idx).unwrap(); - let changed = ins.update(loc.as_mut(), storages, *movetime, *hand_size, grid_size); + let changed = ins.update( + self.item.into_usize(), + loc.as_mut(), + storages, + *movetime, + *hand_size, + grid_size, + ); if changed { // the inserter changed something. diff --git a/src/belt/sushi.rs b/src/belt/sushi.rs index fc298d0..85b8c0f 100644 --- a/src/belt/sushi.rs +++ b/src/belt/sushi.rs @@ -9,6 +9,7 @@ use egui_show_info_derive::ShowInfo; use get_size2::GetSize; use crate::inserter::belt_storage_inserter::Dir; +use crate::item::ITEMCOUNTTYPE; use crate::{ belt::belt::NoSpaceError, item::{IdxTrait, Item, WeakIdxTrait}, @@ -24,8 +25,6 @@ use crate::inserter::FakeUnionStorage; use crate::inserter::belt_storage_inserter_non_const_gen::BeltStorageInserterDyn; use itertools::Either; -use crate::belt::smart::{HAND_SIZE, MOVETIME}; - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct SushiBelt { @@ -48,7 +47,7 @@ pub struct SushiBelt { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub(super) struct SushiInserterStoreDyn { - pub(super) inserters: Box<[(BeltStorageInserterDyn, Item)]>, + pub(super) inserters: Box<[(BeltStorageInserterDyn, Item, u8, ITEMCOUNTTYPE)]>, } #[derive(Debug, PartialEq, Eq)] @@ -82,6 +81,8 @@ impl SushiBelt { filter: Item, pos: BeltLenType, storage_id: FakeUnionStorage, + movetime: u16, + hand_size: ITEMCOUNTTYPE, ) -> Result<(), SpaceOccupiedError> { assert!( usize::from(pos) < self.locs.len(), @@ -92,7 +93,12 @@ impl SushiBelt { let mut pos_after_last_inserter = 0; let mut i = 0; - for offset in self.inserters.inserters.iter().map(|(i, _item)| i.offset) { + for offset in self + .inserters + .inserters + .iter() + .map(|(i, _item, _movetime, _hand_size)| i.offset) + { let next_inserter_pos = pos_after_last_inserter + offset; match next_inserter_pos.cmp(&pos) { @@ -119,6 +125,8 @@ impl SushiBelt { storage_id, ), filter, + movetime.try_into().unwrap_or(u8::MAX), + hand_size, ), ); ins.into_boxed_slice() @@ -126,7 +134,7 @@ impl SushiBelt { let next = self.inserters.inserters.get_mut(i + 1); - if let Some((next_ins, _item)) = next { + if let Some((next_ins, _item, _movetime, _hand_size)) = next { next_ins.offset -= new_inserter_offset + 1; } @@ -138,6 +146,8 @@ impl SushiBelt { filter: Item, pos: BeltLenType, storage_id: FakeUnionStorage, + movetime: u16, + hand_size: ITEMCOUNTTYPE, ) -> Result<(), SpaceOccupiedError> { assert!( usize::from(pos) < self.locs.len(), @@ -148,7 +158,12 @@ impl SushiBelt { let mut pos_after_last_inserter = 0; let mut i = 0; - for offset in self.inserters.inserters.iter().map(|(i, _item)| i.offset) { + for offset in self + .inserters + .inserters + .iter() + .map(|(i, _item, _movetime, _hand_size)| i.offset) + { let next_inserter_pos = pos_after_last_inserter + offset; match next_inserter_pos.cmp(&pos) { @@ -175,6 +190,8 @@ impl SushiBelt { storage_id, ), filter, + movetime.try_into().unwrap_or(u8::MAX), + hand_size, ), ); ins.into_boxed_slice() @@ -182,7 +199,7 @@ impl SushiBelt { let next = self.inserters.inserters.get_mut(i + 1); - if let Some((next_ins, _item)) = next { + if let Some((next_ins, _item, _movetime, _hand_size)) = next { next_ins.offset -= new_inserter_offset + 1; } @@ -193,7 +210,7 @@ impl SushiBelt { pub fn get_inserter_info_at(&self, belt_pos: u16) -> Option { let mut pos = 0; - for (inserter, _item) in self.inserters.inserters.iter() { + for (inserter, _item, movetime, hand_size) in self.inserters.inserters.iter() { pos += inserter.offset; if pos == belt_pos { let (dir, state) = inserter.state.into(); @@ -201,6 +218,9 @@ impl SushiBelt { outgoing: dir == Dir::BeltToStorage, state, connection: inserter.storage_id, + + hand_size: *hand_size, + movetime: *movetime, }); } else if pos > belt_pos { return None; @@ -221,7 +241,12 @@ impl SushiBelt { let mut pos_after_last_inserter = 0; let mut i = 0; - for offset in self.inserters.inserters.iter().map(|(i, _item)| i.offset) { + for offset in self + .inserters + .inserters + .iter() + .map(|(i, _item, _movetime, _hand_size)| i.offset) + { let next_inserter_pos = pos_after_last_inserter + offset; match next_inserter_pos.cmp(&belt_pos) { @@ -243,7 +268,7 @@ impl SushiBelt { pub fn set_inserter_storage_id(&mut self, belt_pos: u16, new: FakeUnionStorage) { let mut pos = 0; - for (inserter, _item) in self.inserters.inserters.iter_mut() { + for (inserter, _item, _movetime, _hand_size) in self.inserters.inserters.iter_mut() { pos += inserter.offset; if pos == belt_pos { inserter.storage_id = new; @@ -265,7 +290,12 @@ impl SushiBelt { let mut pos_after_last_inserter = 0; let mut i = 0; - for offset in self.inserters.inserters.iter().map(|(i, _item)| i.offset) { + for offset in self + .inserters + .inserters + .iter() + .map(|(i, _item, _movetime, _hand_size)| i.offset) + { let next_inserter_pos = pos_after_last_inserter + offset; match next_inserter_pos.cmp(&pos) { @@ -309,7 +339,7 @@ impl SushiBelt { .inserters .inserters .iter() - .map(|(_, item)| *item) + .map(|(_, item, _movetime, _hand_size)| *item) .chain(belt_belt_filter_in.into_iter().filter_map(|info| { let SushiInfo::Pure(item) = info else { unreachable!() @@ -395,12 +425,12 @@ impl SushiBelt { .collect::().into(), inserters: InserterStoreDyn { // FIXME: Some of these inserters might have a different item than what we are converting to. This will result in crashes and item transmutation - inserters: inserters.into_iter().map(|(ins, inserter_item)| { + inserters: inserters.into_iter().map(|(ins, inserter_item, movetime, hand_size)| { assert_eq!(item, inserter_item, "FIXME: We need to handle inserters which will never work again in smart belts"); // if item != inserter_item { // error!("We need to handle inserters which will never work again in smart belts!!!!!!!"); // } - (ins, MOVETIME, HAND_SIZE) + (ins, movetime, hand_size) }).collect(), }, item, @@ -485,7 +515,7 @@ impl SushiBelt { .inserters .inserters .iter() - .map(|(i, _item)| i.offset) + .map(|(i, _item, _movetime, _hand_size)| i.offset) .enumerate(); let mut current_pos = 0; @@ -606,7 +636,7 @@ impl SushiBelt { let free_spots_before_last_inserter_front: u16 = front_inserters .inserters .iter() - .map(|(i, _item)| i.offset) + .map(|(i, _item, _movetime, _hand_size)| i.offset) .sum(); let length_after_last_inserter = TryInto::::try_into(front_len) .expect("Belt should be max u16::MAX long") @@ -614,7 +644,7 @@ impl SushiBelt { - TryInto::::try_into(num_front_inserters) .expect("Belt should be max u16::MAX long"); - if let Some((i, _item)) = back_inserters.inserters.get_mut(0) { + if let Some((i, _item, _movetime, _hand_size)) = back_inserters.inserters.get_mut(0) { i.offset += length_after_last_inserter; } @@ -1047,7 +1077,7 @@ impl Belt for SushiBelt { }); if side == Side::FRONT { - if let Some((i, _ietm)) = self.inserters.inserters.first_mut() { + if let Some((i, _item, _movetime, _hand_size)) = self.inserters.inserters.first_mut() { i.offset -= amount - pos_after_last_removed_inserter; } } diff --git a/src/data/mod.rs b/src/data/mod.rs index d016035..2add391 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1511,33 +1511,34 @@ impl RawDataStore { instantly_finished_technologies, belt_infos: vec![ + // TODO: For now only have one kind of transport belt since connection is still borked BeltInfo { name: "factory_game::fast_transport_belt".to_string().into(), - display_name: "Fast Transport Belt".to_string(), + display_name: "Express Transport Belt".to_string(), has_underground: Some(BeltUndergroundInfo { max_distance: 9 }), has_splitter: None, timer_increase: 45 * 2, }, - BeltInfo { - name: "factory_game::transport_belt".to_string().into(), - display_name: "Transport Belt".to_string(), - has_underground: Some(BeltUndergroundInfo { max_distance: 6 }), - has_splitter: None, - timer_increase: 15 * 2, - }, + // BeltInfo { + // name: "factory_game::transport_belt".to_string().into(), + // display_name: "Transport Belt".to_string(), + // has_underground: Some(BeltUndergroundInfo { max_distance: 6 }), + // has_splitter: None, + // timer_increase: 15 * 2, + // }, ], // FIXME: mining_drill_info: vec![ - MiningDrillInfo { - name: "factory_game::mining_drill".to_string().into(), - display_name: "Electric Mining Drill".to_string().into(), - size: [3, 3], - mining_range: [5, 5], - base_speed: 20, - resource_drain: (1, 1), - output_offset: Some([1, -1]), - }, + // MiningDrillInfo { + // name: "factory_game::mining_drill".to_string().into(), + // display_name: "Electric Mining Drill".to_string().into(), + // size: [3, 3], + // mining_range: [5, 5], + // base_speed: 20, + // resource_drain: (1, 1), + // output_offset: Some([1, -1]), + // }, MiningDrillInfo { name: "factory_game::mining_drill_small_no_output" .to_string() diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index b494cf5..a699881 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -14,6 +14,7 @@ use log::{error, warn}; use petgraph::Directed; use crate::{ + NewWithDataStore, app_state::SimulationState, belt::splitter::SplitterDistributionMode, blueprint::Blueprint, @@ -46,10 +47,63 @@ pub const WIDTH_PER_LEVEL: usize = 16; pub struct Hotbar { slots: [Option>; 10], } -impl Default for Hotbar { - fn default() -> Self { +impl NewWithDataStore for Hotbar { + fn new( + data_store: impl std::borrow::Borrow>, + ) -> Self { Self { - slots: array::from_fn(|_| None), + slots: array::from_fn(|idx| match idx + 1 { + 1 => Some(HeldObject::Entity(PlaceEntityType::Lab { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + 2 => Some(HeldObject::Entity(PlaceEntityType::Assembler { + pos: Position { x: 0, y: 0 }, + ty: 0, + rotation: Dir::North, + })), + 3 => Some(HeldObject::Entity(PlaceEntityType::Belt { + pos: Position { x: 0, y: 0 }, + ty: 0, + direction: Dir::North, + })), + 4 => Some(HeldObject::Entity(PlaceEntityType::Inserter { + pos: Position { x: 0, y: 0 }, + ty: 0, + dir: Dir::North, + filter: None, + user_movetime: None, + })), + 5 => Some(HeldObject::Entity(PlaceEntityType::PowerPole { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + 6 => Some(HeldObject::Entity(PlaceEntityType::Chest { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + 7 => Some(HeldObject::Entity(PlaceEntityType::Underground { + pos: Position { x: 0, y: 0 }, + ty: 0, + direction: Dir::North, + underground_dir: UndergroundDir::Entrance, + })), + 8 => Some(HeldObject::Entity(PlaceEntityType::SolarPanel { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + 9 => Some(HeldObject::Entity(PlaceEntityType::FluidTank { + pos: Position { x: 0, y: 0 }, + ty: 0, + rotation: Dir::North, + })), + 10 => Some(HeldObject::Entity(PlaceEntityType::Beacon { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + + _ => unreachable!(), + }), } } } @@ -222,7 +276,7 @@ impl current_fork_save_in_progress: None, - hotbar: Hotbar::default(), + hotbar: Hotbar::new(data_store), hotbar_window_open: true, } } @@ -1290,7 +1344,7 @@ impl data_store: &DataStore, ) { // Possible Actions - ui.columns_const(|uis: &mut [egui::Ui; 12]| { + ui.columns_const(|uis: &mut [egui::Ui; 13]| { for (i, ui) in uis.iter_mut().enumerate() { let ty_count = match i { 0 => data_store.assembler_info.len(), @@ -1305,6 +1359,7 @@ impl 9 => data_store.solar_panel_info.len(), 10 => data_store.lab_info.len(), 11 => data_store.inserter_infos.len(), + 12 => data_store.mining_drill_info.len(), _ => unreachable!(), } as u8; @@ -1413,6 +1468,14 @@ impl }), &data_store.inserter_infos[ty as usize].display_name, ), + 12 => ( + HeldObject::Entity(PlaceEntityType::MiningDrill { + pos: Position { x: 0, y: 0 }, + ty, + rotation: Dir::North, + }), + &data_store.mining_drill_info[ty as usize].display_name, + ), _ => unreachable!(), }; diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index d19f7c1..d6c53e2 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -419,7 +419,8 @@ fn try_attaching_fluids( conn_fluid, conn_storage, data_store, - ), + ) + .unwrap(), conn_pos, ) }, @@ -431,7 +432,8 @@ fn try_attaching_fluids( conn_fluid, conn_storage, data_store, - ), + ) + .unwrap(), conn_pos, ) }, @@ -3214,17 +3216,13 @@ impl World { - let dest_storage_untranslated = match dest_storage_untranslated { + let dest_storage = match dest_storage_untranslated { Static::Done(storage) => storage, Static::ToInstantiate => { unreachable!("Storages must be instantiated before calling this function") }, }; - let dest_storage = dest_storage_untranslated - .translate(filter, data_store) - .unwrap(); - match simulation_state.factory.belts.add_belt_storage_inserter( filter, start_belt_id, @@ -3233,7 +3231,8 @@ impl World World { - let start_storage_untranslated = match start_storage_untranslated { + let start_storage = match start_storage_untranslated { Static::Done(storage) => storage, Static::ToInstantiate => { unreachable!("Storages must be instantiated before calling this function") }, }; - let start_storage = start_storage_untranslated - .translate(filter, data_store) - .unwrap(); - match simulation_state.factory.belts.add_storage_belt_inserter( filter, dest_belt_id, @@ -3271,7 +3266,8 @@ impl World World { - let start_storage_untranslated = match start_storage_untranslated { + let start_storage = match start_storage_untranslated { Static::Done(storage) => storage, Static::ToInstantiate => { unreachable!("Storages must be instantiated before calling this function") }, }; - let dest_storage_untranslated = match dest_storage_untranslated { + let dest_storage = match dest_storage_untranslated { Static::Done(storage) => storage, Static::ToInstantiate => { unreachable!("Storages must be instantiated before calling this function") }, }; - let start_storage = start_storage_untranslated - .translate(filter, data_store) - .unwrap(); - let dest_storage = dest_storage_untranslated - .translate(filter, data_store) - .unwrap(); - let index = simulation_state.factory.storage_storage_inserters.add_ins( filter, movetime.into(), diff --git a/src/inserter/belt_storage_inserter.rs b/src/inserter/belt_storage_inserter.rs index c41cc76..27a67a9 100644 --- a/src/inserter/belt_storage_inserter.rs +++ b/src/inserter/belt_storage_inserter.rs @@ -82,7 +82,8 @@ impl BeltStorageInserter<{ Dir::BeltToStorage }> { } }, InserterState::WaitingForSpaceInDestination(count) => { - let (max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let (max_insert, old) = + index_fake_union(todo!(), storages, self.storage_id, grid_size); let to_insert = min(count, *max_insert - *old); if to_insert > 0 { @@ -134,7 +135,8 @@ impl BeltStorageInserter<{ Dir::StorageToBelt }> { match self.state { InserterState::WaitingForSourceItems(count) => { - let (_max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let (_max_insert, old) = + index_fake_union(todo!(), storages, self.storage_id, grid_size); let to_extract = min(max_hand_size - count, *old); diff --git a/src/inserter/belt_storage_inserter_non_const_gen.rs b/src/inserter/belt_storage_inserter_non_const_gen.rs index be40213..8a67da5 100644 --- a/src/inserter/belt_storage_inserter_non_const_gen.rs +++ b/src/inserter/belt_storage_inserter_non_const_gen.rs @@ -88,6 +88,7 @@ impl BeltStorageInserterDyn { #[inline(always)] pub fn update( &mut self, + item_id: usize, mut loc: impl DerefMut + Deref, storages: SingleItemStorages, movetime: u8, @@ -109,7 +110,8 @@ impl BeltStorageInserterDyn { } }, DynInserterState::BSWaitingForSpaceInDestination(count) => { - let (max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let (max_insert, old) = + index_fake_union(item_id, storages, self.storage_id, grid_size); let to_insert = min(count, *max_insert - *old); if to_insert > 0 { @@ -144,7 +146,8 @@ impl BeltStorageInserterDyn { false }, DynInserterState::SBWaitingForSourceItems(count) => { - let (_max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let (_max_insert, old) = + index_fake_union(item_id, storages, self.storage_id, grid_size); let to_extract = min(max_hand_size - count, *old); diff --git a/src/inserter/belt_storage_pure_buckets.rs b/src/inserter/belt_storage_pure_buckets.rs index a70a156..2be1991 100644 --- a/src/inserter/belt_storage_pure_buckets.rs +++ b/src/inserter/belt_storage_pure_buckets.rs @@ -613,6 +613,7 @@ impl BucketedStorageStorageInserterStore { } fn handle_waiting_for_item_ins( + item_id: usize, inserter: &mut UpdatingInserter, frontend: &mut BucketedStorageStorageInserterStoreFrontend, storages: SingleItemStorages, @@ -633,7 +634,8 @@ impl BucketedStorageStorageInserterStore { } }, Dir::StorageToBelt => { - let (_max_insert, old) = index_fake_union(storages, inserter.storage_id, grid_size); + let (_max_insert, old) = + index_fake_union(item_id, storages, inserter.storage_id, grid_size); let to_extract = min(inserter.max_hand_size - inserter.current_hand, *old); @@ -663,6 +665,7 @@ impl BucketedStorageStorageInserterStore { } fn handle_waiting_for_space_ins( + item_id: usize, inserter: &mut UpdatingInserter, frontend: &mut BucketedStorageStorageInserterStoreFrontend, storages: SingleItemStorages, @@ -673,7 +676,8 @@ impl BucketedStorageStorageInserterStore { ) -> bool { match DIR { Dir::BeltToStorage => { - let (max_insert, old) = index_fake_union(storages, inserter.storage_id, grid_size); + let (max_insert, old) = + index_fake_union(item_id, storages, inserter.storage_id, grid_size); let to_insert = min(inserter.current_hand, *max_insert - *old); @@ -751,6 +755,7 @@ impl BucketedStorageStorageInserterStore { #[profiling::function] pub fn update( &mut self, + item_id: usize, frontend: &mut BucketedStorageStorageInserterStoreFrontend, storages: SingleItemStorages, belts: &mut [SmartBelt], @@ -808,6 +813,7 @@ impl BucketedStorageStorageInserterStore { Dir::BeltToStorage => { let now_moving = self.waiting_for_item.extract_if(.., |inserter| { Self::handle_waiting_for_item_ins::( + item_id, inserter, frontend, storages, @@ -835,6 +841,7 @@ impl BucketedStorageStorageInserterStore { Dir::StorageToBelt => { let now_moving = self.waiting_for_item.extract_if(.., |inserter| { Self::handle_waiting_for_item_ins::( + item_id, inserter, frontend, storages, @@ -912,6 +919,7 @@ impl BucketedStorageStorageInserterStore { ItemIdxType, { Dir::BeltToStorage }, >( + item_id, inserter, frontend, storages, @@ -944,6 +952,7 @@ impl BucketedStorageStorageInserterStore { ItemIdxType, { Dir::StorageToBelt }, >( + item_id, inserter, frontend, storages, @@ -1312,6 +1321,7 @@ mod test { for (storage, belt) in values.into_iter().zip(belt_ids) { if random::() < 1 { store[item].0.update( + item, &mut frontend[item], &mut [ (max_insert.as_slice(), storages_in[item].as_mut_slice()), @@ -1322,6 +1332,7 @@ mod test { current_tick, ); store[item].1.update( + item, &mut frontend[item], &mut [ (max_insert.as_slice(), storages_in[item].as_mut_slice()), @@ -1394,6 +1405,7 @@ mod test { } } store.0.update( + 0, frontend, &mut [ (max_insert.as_slice(), storage_in.as_mut_slice()), @@ -1404,6 +1416,7 @@ mod test { current_tick, ); store.1.update( + 0, frontend, &mut [ (max_insert.as_slice(), storage_in.as_mut_slice()), diff --git a/src/inserter/mod.rs b/src/inserter/mod.rs index 4da4953..c19b6ec 100644 --- a/src/inserter/mod.rs +++ b/src/inserter/mod.rs @@ -183,7 +183,7 @@ impl FakeUnionStorage { item: Item, storage: Storage, data_store: &DataStore, - ) -> Self { + ) -> Result { let grid_size: usize = grid_size(item, data_store); let static_size: usize = static_size(item, data_store); @@ -195,11 +195,16 @@ impl FakeUnionStorage { recipe_idx_with_this_item, index, } => { - assert!( - recipe_idx_with_this_item.into_usize() - < data_store.num_recipes_with_item[item.into_usize()] - ); - Self { + let recipe_idx_with_this_item = *data_store + .recipe_to_translated_index + .get(&( + Recipe { + id: recipe_idx_with_this_item, + }, + item, + )) + .ok_or(())?; + Ok(Self { index: u32::from(index), grid_or_static_flag: u16::from(grid) .checked_add(u16::try_from(grid_offset).unwrap()) @@ -208,21 +213,21 @@ impl FakeUnionStorage { recipe_idx_with_this_item, )) .unwrap(), - } + }) }, - Storage::Lab { grid, index } => Self { + Storage::Lab { grid, index } => Ok(Self { index: u32::from(index), grid_or_static_flag: u16::from(grid) .checked_add(u16::try_from(grid_offset).unwrap()) .expect("Grid ID too high (would overflow the grid_or_static)"), recipe_idx_with_this_item: data_store.num_recipes_with_item[usize_from(item.id)] as u16, - }, - Storage::Static { index, static_id } => Self { + }), + Storage::Static { index, static_id } => Ok(Self { index: u32::try_from(index).unwrap(), grid_or_static_flag: 0, recipe_idx_with_this_item: static_id, - }, + }), } } } @@ -248,30 +253,6 @@ pub enum Storage { } impl Storage { - pub fn translate( - self, - item: Item, - data_store: &DataStore, - ) -> Option { - match self { - Storage::Assembler { - grid, - recipe_idx_with_this_item, - index, - } => Some(Storage::Assembler { - grid, - recipe_idx_with_this_item: *data_store.recipe_to_translated_index.get(&( - Recipe { - id: recipe_idx_with_this_item, - }, - item, - ))?, - index, - }), - storage => Some(storage), - } - } - pub fn change_grid(self, new_id: PowerGridIdentifier) -> Self { match self { Storage::Assembler { @@ -291,43 +272,13 @@ impl Storage { } } - fn into_inner_and_outer_indices( - self, - num_grids_total: usize, - num_recipes: usize, - grid_size: usize, - ) -> (usize, usize) { - match self { - Storage::Assembler { - grid, - recipe_idx_with_this_item, - index, - } => { - debug_assert!( - usize_from(recipe_idx_with_this_item) < num_recipes, - "The recipe stored in an inserter needs to be translated!" - ); - let outer = Into::::into(grid) * grid_size - + Into::::into(recipe_idx_with_this_item); - (outer, index.try_into().unwrap()) - }, - Storage::Lab { grid, index } => { - let outer = Into::::into(grid) * grid_size + num_recipes; - (outer, index.try_into().unwrap()) - }, - Storage::Static { static_id, index } => { - // debug_assert!(usize::from(static_id) < data_store.num_different_static_containers); - let outer = num_grids_total * grid_size + Into::::into(static_id as u8); - (outer, index.try_into().unwrap()) - }, - } - } - - fn into_inner_and_outer_indices_with_statics_at_zero( + fn into_inner_and_outer_indices_with_statics_at_zero( self, + item: Item, num_recipes: usize, grid_size: usize, static_size: usize, + data_store: &DataStore, ) -> (usize, usize) { let grid_offset = static_size.div_ceil(grid_size); @@ -337,6 +288,15 @@ impl Storage { recipe_idx_with_this_item, index, } => { + let recipe_idx_with_this_item = *data_store + .recipe_to_translated_index + .get(&( + Recipe { + id: recipe_idx_with_this_item, + }, + item, + )) + .unwrap(); debug_assert!( usize_from(recipe_idx_with_this_item) < num_recipes, "The recipe stored in an inserter needs to be translated!" @@ -378,6 +338,17 @@ pub enum StaticID { PureSoloOwnedMiningDrill = 1, } +impl TryFrom for StaticID { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Chest), + 1 => Ok(Self::PureSoloOwnedMiningDrill), + _ => Err(()), + } + } +} + #[cfg(test)] mod test { use crate::DATA_STORE; @@ -443,28 +414,28 @@ mod test { proptest! { - #[test] - fn storage_and_fake_union_result_in_same_indices((item, num_grids, storage) in union_test_input()) { - let grid_size = grid_size(item, &DATA_STORE); + // #[test] + // fn storage_and_fake_union_result_in_same_indices((item, num_grids, storage) in union_test_input()) { + // let grid_size = grid_size(item, &DATA_STORE); - let storage_union = FakeUnionStorage::from_storage(item, storage, &DATA_STORE); + // let storage_union = FakeUnionStorage::from_storage(item, storage, &DATA_STORE); - let union_indices = storage_union.into_inner_and_outer_indices(num_grids.into(), grid_size); + // let union_indices = storage_union.into_inner_and_outer_indices(num_grids.into(), grid_size); - let storage_indices = storage.into_inner_and_outer_indices(num_grids.into(), DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size); + // let storage_indices = storage.into_inner_and_outer_indices(Item::try_from(0).unwrap(), num_grids.into(), DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size, &DATA_STORE); - prop_assert_eq!(union_indices, storage_indices); - } + // prop_assert_eq!(union_indices, storage_indices); + // } #[test] fn storage_and_fake_union_result_in_same_indices_with_statics_at_zero((item, _num_grids, storage) in union_test_input()) { let grid_size = grid_size(item, &DATA_STORE); - let storage_union = FakeUnionStorage::from_storage_with_statics_at_zero(item, storage, &DATA_STORE); + let storage_union = FakeUnionStorage::from_storage_with_statics_at_zero(item, storage, &DATA_STORE).unwrap(); let union_indices = storage_union.into_inner_and_outer_indices_with_statics_at_zero(grid_size); - let storage_indices = storage.into_inner_and_outer_indices_with_statics_at_zero(DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size, static_size(item, &DATA_STORE)); + let storage_indices = storage.into_inner_and_outer_indices_with_statics_at_zero(item, DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size, static_size(item, &DATA_STORE), &DATA_STORE); prop_assert_eq!(union_indices, storage_indices); } diff --git a/src/inserter/storage_storage_inserter.rs b/src/inserter/storage_storage_inserter.rs index 9b1f087..8eb259c 100644 --- a/src/inserter/storage_storage_inserter.rs +++ b/src/inserter/storage_storage_inserter.rs @@ -43,6 +43,7 @@ impl StorageStorageInserter { pub fn update( &mut self, + item_id: usize, storages: SingleItemStorages, movetime: u8, max_hand_size: ITEMCOUNTTYPE, @@ -53,7 +54,8 @@ impl StorageStorageInserter { match self.state { InserterState::WaitingForSourceItems(count) => { - let (_max_insert, old) = index_fake_union(storages, self.storage_id_in, grid_size); + let (_max_insert, old) = + index_fake_union(item_id, storages, self.storage_id_in, grid_size); let to_extract = min(max_hand_size - count, *old); @@ -69,7 +71,8 @@ impl StorageStorageInserter { } }, InserterState::WaitingForSpaceInDestination(count) => { - let (max_insert, old) = index_fake_union(storages, self.storage_id_out, grid_size); + let (max_insert, old) = + index_fake_union(item_id, storages, self.storage_id_out, grid_size); let to_insert = min(count, *max_insert - *old); diff --git a/src/inserter/storage_storage_with_buckets.rs b/src/inserter/storage_storage_with_buckets.rs index 53a2ac4..8492fbc 100644 --- a/src/inserter/storage_storage_with_buckets.rs +++ b/src/inserter/storage_storage_with_buckets.rs @@ -653,7 +653,8 @@ impl BucketedStorageStorageInserterStore { _current_tick: u32, _movetime: u16, ) -> bool { - let (_max_insert, old) = index_fake_union(storages, inserter.storage_id_in, grid_size); + let (_max_insert, old) = + index_fake_union(todo!(), storages, inserter.storage_id_in, grid_size); let to_extract = min(inserter.max_hand_size - inserter.current_hand, *old); @@ -686,7 +687,8 @@ impl BucketedStorageStorageInserterStore { _current_tick: u32, _movetime: u16, ) -> bool { - let (max_insert, old) = index_fake_union(storages, inserter.storage_id_out, grid_size); + let (max_insert, old) = + index_fake_union(todo!(), storages, inserter.storage_id_out, grid_size); let to_insert = min(inserter.current_hand, *max_insert - *old); diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 305ffc5..475a111 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -272,6 +272,7 @@ impl BucketedStorageStorageInserterStore { } fn handle_waiting_for_item_ins( + item_id: usize, inserter: &mut InserterState, bucket_data: &mut InserterBucketData, @@ -282,7 +283,7 @@ impl BucketedStorageStorageInserterStore { ) -> bool { let storage_id = bucket_data.storage_id_in; - let (_max_insert, old) = index_fake_union(storages, storage_id, grid_size); + let (_max_insert, old) = index_fake_union(item_id, storages, storage_id, grid_size); let old_val = *old; let max_hand_size = bucket_data.max_hand_size; @@ -309,6 +310,7 @@ impl BucketedStorageStorageInserterStore { } fn handle_waiting_for_space_ins( + item_id: usize, inserter: &mut InserterState, bucket_data: &mut InserterBucketData, @@ -319,7 +321,7 @@ impl BucketedStorageStorageInserterStore { ) -> bool { let storage_id = bucket_data.storage_id_out; - let (max_insert, old) = index_fake_union(storages, storage_id, grid_size); + let (max_insert, old) = index_fake_union(item_id, storages, storage_id, grid_size); let old_val = *old; let max_insert = *max_insert; @@ -456,6 +458,7 @@ impl BucketedStorageStorageInserterStore { ); let now_moving = self.waiting_for_item.extract_if(start..end, |inserter| { Self::handle_waiting_for_item_ins( + item_id, &mut self.inserters[inserter.index.index as usize], inserter, storages, @@ -529,6 +532,7 @@ impl BucketedStorageStorageInserterStore { self.waiting_for_space_in_destination .extract_if(start..end, |inserter| { Self::handle_waiting_for_space_ins( + item_id, &mut self.inserters[inserter.index.index as usize], inserter, storages, diff --git a/src/lab.rs b/src/lab.rs index ca53c87..969f3a6 100644 --- a/src/lab.rs +++ b/src/lab.rs @@ -26,7 +26,7 @@ pub struct MultiLabStore { pub sciences: Box<[Vec]>, timer: Vec, prod_timer: Vec, - holes: Vec, + pub holes: Vec, /// Base Crafting Speed in 5% increments /// i.e. 28 => 140% Crafting speed diff --git a/src/liquid/mod.rs b/src/liquid/mod.rs index c199187..3a4cc58 100644 --- a/src/liquid/mod.rs +++ b/src/liquid/mod.rs @@ -682,6 +682,7 @@ impl FluidSystemStore { { if *inc == old_storage { *inc = new_storage; + log::trace!("Found connection to update"); } } for outgoing in self.fluid_systems_with_fluid[fluid.into_usize()][id.index] @@ -693,10 +694,13 @@ impl FluidSystemStore { { if *outgoing == old_storage { *outgoing = new_storage; + log::trace!("Found connection to update"); } } }, - None => {}, + None => { + log::trace!("No need to update fluid connections for empty fluid network"); + }, } } @@ -1553,6 +1557,7 @@ impl FluidSystem { } pub fn update_fluid_system( + item_id: usize, hot_data: &mut FluidSystemHotData, storages: SingleItemStorages, grid_size: usize, @@ -1566,7 +1571,7 @@ pub fn update_fluid_system( if hot_data.current_fluid_level == 0 { break; } - let (max, data) = index_fake_union(storages, outgoing_conn, grid_size); + let (max, data) = index_fake_union(item_id, storages, outgoing_conn, grid_size); let amount_wanted = *max - *data; let amount_extracted = min( @@ -1592,7 +1597,7 @@ pub fn update_fluid_system( if hot_data.current_fluid_level == hot_data.storage_capacity { break; } - let (_max, data) = index_fake_union(storages, incoming_conn, grid_size); + let (_max, data) = index_fake_union(item_id, storages, incoming_conn, grid_size); let amount_wanted = *data; let amount_extracted = min( diff --git a/src/par_generation.rs b/src/par_generation.rs index 62cd876..5634961 100644 --- a/src/par_generation.rs +++ b/src/par_generation.rs @@ -1425,7 +1425,8 @@ fn pipe_stage( _ => unreachable!(), }, data_store, - ), + ) + .unwrap(), pos, ) }), diff --git a/src/power/mod.rs b/src/power/mod.rs index 0d84e66..1126760 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -341,12 +341,51 @@ impl PowerGridStorage true, + _ => false, + }) + .count(); + let num_labs_in_sim = + grid.lab_stores.sciences[0].len() - grid.lab_stores.holes.len(); + + assert_eq!(num_labs_in_list, num_labs_in_graph); + assert_eq!(num_labs_in_list, num_labs_in_sim); + + let num_assemblers_in_list: usize = + grid.num_assemblers_of_type.iter().copied().sum(); + let num_assemblers_in_graph = grid + .grid_graph + .weak_components() + .filter(|grid_entity| match grid_entity { + (_, PowerGridEntity::Assembler { .. }) => true, + _ => false, + }) + .count(); + let num_assemblers_in_sim: usize = grid.stores.num_assemblers(); + + assert_eq!(num_assemblers_in_list, num_assemblers_in_graph); + assert_eq!(num_assemblers_in_list, num_assemblers_in_sim); + } + } let mut connected_poles: Vec<_> = connected_poles.into_iter().collect(); let ret = if !connected_poles.is_empty() { // Find the largest grid, and choose it as the base // If the size is a tossup pick the one with the smaller grid_id to reduce holes in the power_grid list - let grid = connected_poles + let kept_grid_id = connected_poles .iter() .map(|pos| self.pole_pos_to_grid_id[pos]) .max_by_key(|grid_id| { @@ -364,7 +403,7 @@ impl PowerGridStorage PowerGridStorage>() { // No need to merge a grid with itself. - if other_grid == grid { + if other_grid == kept_grid_id { continue; } ran_once = true; let storage_updates = self .merge_power_grids( - grid, + kept_grid_id, other_grid, data_store, pole_position, connected_poles.iter().copied(), ) .into_iter() - .flatten(); + .flatten() + .map(|update| { + // TODO: Make debug assert + assert_eq!(update.new_grid, kept_grid_id); + assert_eq!(update.old_grid, other_grid); + + update + }); + let old_len = storage_update_vec.len(); storage_update_vec.extend(storage_updates); + let new_updates = storage_update_vec.len() - old_len; } assert!(ran_once); #[cfg(debug_assertions)] { - for key in self.power_grids[grid as usize].grid_graph.keys() { + for key in self.power_grids[kept_grid_id as usize].grid_graph.keys() { if let Some(index) = connected_poles.iter().position(|v| v == key) { connected_poles.remove(index); } @@ -419,7 +467,7 @@ impl PowerGridStorage PowerGridStorage { max_lazy_power: Watt, pub last_power_mult: u8, + pub power_mult_at_last_beacon_update: u8, pub power_mult_history: Timeline, // FIXME: Not actually storing where the power consumption/production originates is not very useful :/ // pub power_consumption_history: Timeline, @@ -209,6 +210,7 @@ impl PowerGrid PowerGrid PowerGrid PowerGrid= other.power_mult_at_last_beacon_update { + self.power_mult_at_last_beacon_update + } else { + other.power_mult_at_last_beacon_update + } + }, power_mult_history: { if self.last_power_consumption >= other.last_power_consumption { self.power_mult_history @@ -2312,6 +2324,7 @@ impl PowerGrid, ) -> ( ResearchProgress, @@ -2641,23 +2654,65 @@ impl PowerGrid, (_, _, _))> = if next_power_mult - < MIN_BEACON_POWER_MULT - && self.last_power_mult >= MIN_BEACON_POWER_MULT - { - // Disable beacons (But keep power consumption modifier unchanged, to prevent flickering) - self.beacon_affected_entities - .iter() - .map(|(k, v)| (*k, (-v.0, -v.1, -0))) - .collect() - } else if next_power_mult >= MIN_BEACON_POWER_MULT - && self.last_power_mult < MIN_BEACON_POWER_MULT - { - // Enable beacons (But keep power consumption modifier unchanged, to prevent flickering) - self.beacon_affected_entities - .iter() - .map(|(k, v)| (*k, (v.0, v.1, 0))) - .collect() + // let beacon_updates: Vec<(BeaconAffectedEntity<_>, (_, _, _))> = if next_power_mult + // < MIN_BEACON_POWER_MULT + // && self.last_power_mult >= MIN_BEACON_POWER_MULT + // { + // // Disable beacons (But keep power consumption modifier unchanged, to prevent flickering) + // self.beacon_affected_entities + // .iter() + // .map(|(k, v)| (*k, (-v.0, -v.1, -0))) + // .collect() + // } else if next_power_mult >= MIN_BEACON_POWER_MULT + // && self.last_power_mult < MIN_BEACON_POWER_MULT + // { + // // Enable beacons (But keep power consumption modifier unchanged, to prevent flickering) + // self.beacon_affected_entities + // .iter() + // .map(|(k, v)| (*k, (v.0, v.1, 0))) + // .collect() + // } else { + // vec![] + // }; + + // This is scaling beacon effect linearly + // FIXME: For this to be correct, when adding a beacon/adding modules to beacon etc, we need to calculate the effect the same way + + // TODO: AFAIK Factorio does not update the effects of beacons every tick but more sparsely (to save UPS) + // For now I will do the same, and only update the beacon effectiveness every 60 ticks (1/seconds) + // We are still much more effective than Factorio here since AFAIK, they need to update beacosn even if the power satisfaction (and as such the beacon effect) did not change + // In that case I can just not do any updates + let beacon_updates = if current_tick % 60 == 0 { + let ret = if next_power_mult != self.power_mult_at_last_beacon_update { + profiling::scope!("Generate Beacon updates"); + self.beacon_affected_entities + .iter() + .map(|(&entity, &effect)| { + let effect: [i16; 3] = effect.into(); + let old_effect = effect.map(|e| { + i32::from(e) * i32::from(self.power_mult_at_last_beacon_update) / 64 + }); + let new_effect = + effect.map(|e| i32::from(e) * i32::from(next_power_mult) / 64); + + let change = old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .map(|v| v.try_into().unwrap()) + .collect_array() + .unwrap(); + + (entity, change.into()) + }) + .collect() + } else { + vec![] + }; + + self.power_mult_at_last_beacon_update = next_power_mult; + + ret } else { vec![] }; diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index 85b3864..10a12c9 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -325,7 +325,7 @@ impl eframe::App for App { game_state_receiver: recv, }; } - } + } // else if ui.button("Load Debug Save").clicked() { // #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] // if let Some(path) = rfd::FileDialog::new() diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs index d3782ca..62a9ac8 100644 --- a/src/rendering/mod.rs +++ b/src/rendering/mod.rs @@ -169,6 +169,8 @@ pub struct TextureAtlas { underground: enum_map::EnumMap>, + accumulator: EntitySprite, + mining_drill: EntitySprite, solar_panel: EntitySprite, @@ -273,6 +275,7 @@ fn texture_atlas() -> TextureAtlas { no_power: entity_sprite_from_path_scaled!("temp_assets/no_power.png", 1, 1.0), assembler: entity_sprite_from_path_tiling!("temp_assets/assembler.png", 1), + accumulator: entity_sprite_from_path_tiling!("temp_assets/assembler.png", 1), chest: entity_sprite_from_path_tiling!("temp_assets/outside_world.png", 1), items: vec![sprite_from_path!("temp_assets/plate.png", 1); 200].into_boxed_slice(), @@ -495,6 +498,8 @@ fn texture_atlas() -> TextureAtlas { 1.0 / 2.0 ), + accumulator: entity_sprite_from_path_tiling!("temp_assets/krastorio/energy-storage.png", 1), + lab: entity_sprite_from_path_tiling!("temp_assets/krastorio/advanced-lab.png", 1), dark_square: sprite_from_path!("temp_assets/dark_square.png", 1), diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 30ee5a2..7f57a49 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1,6 +1,6 @@ use crate::belt::belt::Belt; -use crate::belt::smart::{HAND_SIZE, NUM_BELT_LOCS_SEARCHED, SmartBelt}; use crate::belt::smart::{NUM_BELT_FREE_CACHE_HITS, NUM_BELT_UPDATES}; +use crate::belt::smart::{NUM_BELT_LOCS_SEARCHED, SmartBelt}; use crate::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; use crate::frontend::action::action_state_machine::ForkSaveInfo; @@ -909,11 +909,16 @@ pub fn render_world( let movetime: u16 = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks).into(); match info { crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => { - let hand_size = HAND_SIZE; let Some(state) = game_state.simulation_state.factory.belts.get_inserter_info_at(*id, *belt_pos) else { error!("Could not get rendering info for inserter!"); continue; }; + let hand_size = state.hand_size; + + // TODO: Due to clamping this does not currently hold: + // assert_eq!(movetime, u16::from(state.movetime)); + let movetime = u16::from(state.movetime); + let item = game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); let (mut position, items): (f32, ITEMCOUNTTYPE) = match state.state { @@ -960,7 +965,7 @@ pub fn render_world( // TODO: }, crate::frontend::world::tile::AttachedInserter::StorageStorage { item, inserter } => { - let hand_size = HAND_SIZE; + let hand_size = data_store.inserter_infos[*ty as usize].base_hand_size; let item = *item; let state = game_state.simulation_state.factory.storage_storage_inserters.get_inserter(item, movetime, *inserter, current_tick); @@ -1205,7 +1210,7 @@ pub fn render_world( [usize::from(*ty)].size, 0, entity_layer); }, Entity::Accumulator { ty, pos, .. } => { - texture_atlas.chest.draw([ + texture_atlas.accumulator.draw([ draw_offset.0 + pos.x as f32, draw_offset.1 + pos.y as f32, ], data_store.solar_panel_info @@ -1702,7 +1707,7 @@ pub fn render_world( ); }, crate::frontend::world::tile::PlaceEntityType::Accumulator { pos, ty } => { - texture_atlas.chest.draw( + texture_atlas.accumulator.draw( [draw_offset.0 + pos.x as f32, draw_offset.1 + pos.y as f32], data_store.accumulator_info[usize::from(*ty)].size, 0, @@ -2201,7 +2206,6 @@ pub fn render_ui< lower_dec = 1.0; } - dbg!(lower_dec); lower_dec = lower_dec * ticks_per_value / 60.0 / 60.0; (0..40) @@ -2299,7 +2303,7 @@ pub fn render_ui< actions.push(ActionType::Remove(assembler_pos)); - let area = data_store.mining_drill_info[1].size(*rotation); + let area = data_store.mining_drill_info[0].size(*rotation); for x in assembler_pos.x..(assembler_pos.x + i32::from(area[0])) { for y in assembler_pos.y..(assembler_pos.y + i32::from(area[1])) { @@ -2332,7 +2336,7 @@ pub fn render_ui< entities: EntityPlaceOptions::Single(PlaceEntityType::MiningDrill { pos: assembler_pos, rotation: inserter_rotation, - ty: 1, + ty: 0, }), force: false, })); diff --git a/src/rendering/temp_assets/krastorio/energy-storage.png b/src/rendering/temp_assets/krastorio/energy-storage.png new file mode 100644 index 0000000000000000000000000000000000000000..8629c67186320e88a65a97e31173a36a32e07523 GIT binary patch literal 71532 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 O0000( #[inline(always)] pub fn index_fake_union<'a, 'b>( + item_id: usize, slice: SingleItemStorages<'a, 'b>, storage_id: FakeUnionStorage, grid_size: usize, @@ -144,7 +147,94 @@ pub fn index_fake_union<'a, 'b>( "Out slice was out of bounds for storage_id {storage_id:?}. len was {len}, index was {outer}, grid_size was {grid_size}.", ); }; - (&subslice.0[inner], &mut subslice.1[inner]) + + let Some(max_insert) = subslice.0.get(inner) else { + let item = Item { + id: item_id.try_into().unwrap(), + }; + let static_size: usize = static_size(item, &DATA_STORE); + let is_static = (storage_id.grid_or_static_flag as usize) < static_size; + let index = storage_id.index; + + if !is_static { + let grid_id = storage_id.grid_or_static_flag as usize - static_size; + + let recipe = DATA_STORE.recipe_to_translated_index.iter().find( + |((_recipe, found_item), index)| { + item == *found_item + && u16::from(**index) == storage_id.recipe_idx_with_this_item + }, + ); + + if let Some(((r, _), _)) = recipe { + // we are a assembler + panic!( + "Failed FakeUnion Index for item {} for Assembler in grid {}, with recipe {} and index {}", + &DATA_STORE.item_names[item_id], + grid_id, + DATA_STORE.recipe_names[r.into_usize()], + index + ); + } else { + // We are a lab + panic!( + "Failed FakeUnion Index for item {} for Lab in grid {}, with index {}", + &DATA_STORE.item_names[item_id], grid_id, index + ); + } + } else { + let static_id = StaticID::try_from(storage_id.recipe_idx_with_this_item as u8).unwrap(); + + panic!( + "Failed FakeUnion Index for item {} for Static {:?}, with index {}", + &DATA_STORE.item_names[item_id], static_id, index + ); + } + }; + let Some(items) = subslice.1.get_mut(inner) else { + let item = Item { + id: item_id.try_into().unwrap(), + }; + let static_size: usize = static_size(item, &DATA_STORE); + let is_static = (storage_id.grid_or_static_flag as usize) < static_size; + let index = storage_id.index; + + if !is_static { + let grid_id = storage_id.grid_or_static_flag as usize - static_size; + + let recipe = DATA_STORE.recipe_to_translated_index.iter().find( + |((_recipe, found_item), index)| { + item == *found_item + && u16::from(**index) == storage_id.recipe_idx_with_this_item + }, + ); + + if let Some(((r, _), _)) = recipe { + // we are a assembler + panic!( + "Failed FakeUnion Index for item {} for Assembler in grid {}, with recipe {} and index {}", + &DATA_STORE.item_names[item_id], + grid_id, + DATA_STORE.recipe_names[r.into_usize()], + index + ); + } else { + // We are a lab + panic!( + "Failed FakeUnion Index for item {} for Lab in grid {}, with index {}", + &DATA_STORE.item_names[item_id], grid_id, index + ); + } + } else { + let static_id = StaticID::try_from(storage_id.recipe_idx_with_this_item as u8).unwrap(); + + panic!( + "Failed FakeUnion Index for item {} for Static {:?}, with index {}", + &DATA_STORE.item_names[item_id], static_id, index + ); + } + }; + (max_insert, items) } #[profiling::function] diff --git a/test_blueprints/solar_tile.bp b/test_blueprints/solar_tile.bp index 392ce4e..3934598 100644 --- a/test_blueprints/solar_tile.bp +++ b/test_blueprints/solar_tile.bp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c00805e515c34c57351071faa8c70b8891f0541ad4bae04c6f4ec0fec70ef02 -size 592 +oid sha256:9ffa0b20b177f29b19871a3b201fb430ef7892c99cb63e83248fec42604e3ec1 +size 572 From db9fa06f7f71d748823de99747fb8cc86693f603 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 00:14:37 +0000 Subject: [PATCH 3/3] Bump ron from 0.8.1 to 0.10.1 Bumps [ron](https://github.com/ron-rs/ron) from 0.8.1 to 0.10.1. - [Release notes](https://github.com/ron-rs/ron/releases) - [Changelog](https://github.com/ron-rs/ron/blob/master/CHANGELOG.md) - [Commits](https://github.com/ron-rs/ron/compare/v0.8.1...v0.10.1) --- updated-dependencies: - dependency-name: ron dependency-version: 0.10.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 28 +++++----------------------- Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2e6601..ae9971a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -591,12 +591,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -1369,7 +1363,7 @@ dependencies = [ "log", "nohash-hasher", "profiling", - "ron 0.10.1", + "ron", "serde", "unicode-segmentation", ] @@ -1694,7 +1688,7 @@ dependencies = [ name = "factory" version = "0.2.1" dependencies = [ - "base64 0.22.1", + "base64", "bimap", "bincode 2.0.1", "bitcode", @@ -1740,7 +1734,7 @@ dependencies = [ "rayon", "recycle_vec", "rfd", - "ron 0.8.1", + "ron", "rstest", "rustc-hash 2.1.1", "serde", @@ -4373,25 +4367,13 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.10.0", - "serde", - "serde_derive", -] - [[package]] name = "ron" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" dependencies = [ - "base64 0.22.1", + "base64", "bitflags 2.10.0", "serde", "serde_derive", @@ -5453,7 +5435,7 @@ version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" dependencies = [ - "base64 0.22.1", + "base64", "data-url", "flate2", "fontdb", diff --git a/Cargo.toml b/Cargo.toml index 37f5add..bd55d37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ simple_logger = {version = "5.0.0", optional = true } rayon = "1.10.0" serde = { version = "1.0.217", features = ["derive"], default-features = false } directories = "6.0.0" -ron = "0.8.1" +ron = "0.10.1" take_mut = "0.2.2" static_assertions = "1.1.0" itertools = "0.14.0"